improvement: rewrite package search and storage to use sqlite

improvement: clean up a bunch of unused code
This commit is contained in:
Zach Daniel 2023-10-12 18:08:34 -04:00
parent 3728a9ce17
commit 4a37edd47e
180 changed files with 2886 additions and 2860 deletions

View file

@ -4,6 +4,7 @@
:phoenix,
:ash,
:ash_postgres,
:ash_sqlite,
:ash_graphql,
:surface,
:ash_admin,
@ -23,7 +24,6 @@
locals_without_parens: [
has_name_attribute?: 1,
name_attribute: 1,
library_version_attribute: 1,
load_for_search: 1,
doc_attribute: 1,
render_attributes: 1,

1
.gitignore vendored
View file

@ -35,6 +35,7 @@ npm-debug.log
/assets/node_modules/
.elixir_ls
ash-hq.db*
/indexes/*

View file

@ -1,4 +1,4 @@
FROM hexpm/elixir:1.15.4-erlang-26.0.2-ubuntu-bionic-20230126
FROM hexpm/elixir:1.15.4-erlang-26.0.2-ubuntu-focal-20230126
# install build dependencies
USER root
RUN apt-get update
@ -13,9 +13,13 @@ RUN apt-get install -y g++
RUN apt-get install -y make
RUN apt-get install -y curl
RUN apt-get install -y build-essential
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get install -y esl-erlang
RUN apt-get install -y apt-transport-https
RUN apt-get install -y ca-certificates
RUN apt-get install -y fuse3 libfuse3-dev libglib2.0-dev
RUN apt-get install -y sqlite3
COPY --from=flyio/litefs:0.5 /usr/local/bin/litefs /usr/local/bin/litefs
ENV NODE_MAJOR=16
RUN mkdir -p /etc/apt/keyrings
RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
@ -45,4 +49,5 @@ COPY ./config/runtime.exs config/runtime.exs
COPY ./rel ./rel
RUN mix release --overwrite
RUN mkdir indexes
CMD ["_build/prod/rel/ash_hq/bin/ash_hq", "start"]
COPY ./litefs.yml ./litefs.yml
ENTRYPOINT litefs mount

View file

@ -8,7 +8,7 @@
import Config
config :ash_hq,
ecto_repos: [AshHq.Repo]
ecto_repos: [AshHq.Repo, AshHq.SqliteRepo]
config :ash, allow_flow: true
@ -27,9 +27,7 @@ config :appsignal, :config, revision: "test-4"
config :ash_hq,
ash_apis: [
AshHq.Accounts,
AshHq.Ashley,
AshHq.Blog,
AshHq.Discord,
AshHq.Docs,
AshHq.Github,
AshHq.MailingList
@ -88,8 +86,6 @@ config :esbuild,
env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
]
config :open_ai, :http_client_impl, AshHq.Ashley.HttpClient
# Configures Elixir's Logger
config :logger, :console,
format: "$time $metadata[$level] $message\n",

View file

@ -22,6 +22,12 @@ config :git_ops,
manage_readme_version: "README.md",
version_tag_prefix: "v"
config :ash_hq, AshHq.SqliteRepo,
database: Path.join(__DIR__, "../ash-hq.db"),
port: 5432,
show_sensitive_data_on_connection_error: true,
pool_size: 10
config :ash_hq, :show_search_ranking, true
secret_key_base = "FxKFwVYhDFah3bLLXXqWdpdcLf5e5T1UyVM6XQp7kCt/Reg5yuAEI3upAVDRoP5e"

View file

@ -20,6 +20,12 @@ config :ash_hq, :analytics?, true
config :ash_hq, :download_ua_on_start, true
if config_env() == :prod do
config :ash_hq, AshHq.SqliteRepo,
database: "/litefs/db",
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
end
# ## SSL Support
#
# To get SSL working, you will need to add the `https` key

View file

@ -12,9 +12,6 @@ if System.get_env("PHX_SERVER") && System.get_env("RELEASE_NAME") do
config :ash_hq, AshHqWeb.Endpoint, server: true
end
config :open_ai,
api_key: System.get_env("OPEN_API_API_KEY")
config :ash_hq, :github,
api_key: System.get_env("GITHUB_API_KEY"),
client_id: System.get_env("GITHUB_CLIENT_ID"),

View file

@ -24,6 +24,10 @@ config :ash_hq, AshHqWeb.Endpoint,
secret_key_base: secret_key_base,
server: false
config :ash_hq, AshHq.SqliteRepo,
database: Path.join(__DIR__, "../ash-hq#{System.get_env("MIX_TEST_PARTITION")}.db"),
pool_size: 10
config :ash_hq, cloak_key: "J6ED3yBWjlaOW/5byrukZTEryKa++yXWblJuhP91Qq8="
# In test we don't send emails.

View file

@ -6,12 +6,13 @@ kill_signal = "SIGINT"
kill_timeout = 5
processes = []
[deploy]
release_command = "_build/prod/rel/ash_hq/bin/ash_hq eval 'AshHq.Release.migrate'"
[env]
RELEASE_COOKIE = "VsipafjUVIYVpiYiljPg6DNZB8XiSnEf4zLi8WOf9bAU0XK7HuHQqA=="
[mounts]
source = "litefs"
destination = "/var/lib/litefs"
[[services]]
internal_port = 4000
protocol = "tcp"

View file

@ -192,10 +192,6 @@ defmodule AshHq.Accounts.User do
attribute :shirt_size, :string
attribute :github_info, :map
attribute :ashley_access, :boolean do
default false
end
create_timestamp :created_at
update_timestamp :updated_at
end

View file

@ -34,6 +34,7 @@ defmodule AshHq.Application do
AshHq.Vault,
# Start the Ecto repository
AshHq.Repo,
AshHq.SqliteRepo,
# Start the Telemetry supervisor
AshHqWeb.Telemetry,
# Start the PubSub system
@ -42,6 +43,8 @@ defmodule AshHq.Application do
AshHqWeb.Endpoint,
{AshHq.Docs.Library.Agent, nil},
{Cluster.Supervisor, [topologies, [name: AshHq.ClusterSupervisor]]},
{Haystack.Storage.ETS, storage: AshHq.Docs.Indexer.storage()},
AshHq.Docs.Indexer,
AshHq.Github.Monitor
# Start a worker by calling: AshHq.Worker.start_link(arg)
# {AshHq.Worker, arg}

View file

@ -1,12 +0,0 @@
defmodule AshHq.Ashley do
@moduledoc false
use Ash.Api
resources do
registry(AshHq.Ashley.Registry)
end
authorization do
authorize(:by_default)
end
end

View file

@ -1,51 +0,0 @@
defmodule AshHq.Ashley.HttpClient do
@moduledoc false
alias OpenAI.Behaviours.HttpClientBehaviour
alias OpenAI.Error
@behaviour HttpClientBehaviour
@impl HttpClientBehaviour
def request(_, _, _, %{stream: true}, _) do
{:error,
%Error{
message: "Streaming server-sent events is not currently supported by this client."
}}
end
def request(method, url, headers, params, opts) do
case do_request(method, url, headers, params, opts) do
{:ok, %Finch.Response{body: body}} -> {:ok, body}
{:error, error} -> {:error, error}
end
end
@impl HttpClientBehaviour
def multipart_request(:post, url, headers, multipart, opts) do
body_stream = Multipart.body_stream(multipart)
content_type = Multipart.content_type(multipart, "multipart/form-data")
content_length = Multipart.content_length(multipart)
headers = [
{"Content-Type", content_type},
{"Content-Length", to_string(content_length)} | headers
]
request(:post, url, headers, {:stream, body_stream}, opts)
end
defp do_request(method, url, headers, nil, opts) do
Finch.build(method, url, headers, nil, opts) |> Finch.request(OpenAI.Finch)
end
defp do_request(:post, url, headers, {:stream, _} = params, opts) do
Finch.build(:post, url, headers, params, opts) |> Finch.request(OpenAI.Finch)
end
defp do_request(method, url, headers, params, opts) do
Finch.build(method, url, headers, Jason.encode!(params), opts)
|> Finch.request(OpenAI.Finch, receive_timeout: :infinity)
end
end

View file

@ -1,54 +0,0 @@
defmodule AshHq.Ashley.OpenAi do
@moduledoc false
@open_ai_embed_model "text-embedding-ada-002"
@open_ai_chat_model "gpt-4"
@message_token_limit 7000
@dialyzer {:nowarn_function, {:complete, 4}}
def create_embeddings(embeddings) do
OpenAI.Embeddings.create(@open_ai_embed_model, embeddings, user: "ash-hq-importer")
end
def complete(system_message, system_message_tokens, messages, user_email) do
OpenAI.Chat.create_completion(
@open_ai_chat_model,
[
%{role: :system, content: system_message}
| fit_to_tokens(messages, @message_token_limit - system_message_tokens)
],
user: user_email,
temperature: 0.2
)
end
defp fit_to_tokens(messages, remaining) do
messages
|> Enum.reverse()
|> Enum.reduce_while({remaining, []}, fn message, {remaining, stack} ->
tokens = tokens(message)
if tokens <= remaining do
{:cont, {remaining - tokens, [message | stack]}}
else
{:halt, {remaining, stack}}
end
end)
|> elem(1)
end
def tokens(%{content: message}) do
tokens(message)
end
def tokens(message) do
# Can't link to my python, so I'm just making a conservative estimate
# a token is ~4 chars
# Tiktoken.CL100K.encode_ordinary(message)
message
|> String.length()
|> div(3)
|> Kernel.+(4)
end
end

View file

@ -1,14 +0,0 @@
defmodule AshHq.Ashley.Pinecone do
@moduledoc false
@pinecone_opts [
environment: "eu-west1-gcp",
project: "ba28bca",
index: "ash-hq-docs"
]
def client do
Pinecone.Client.new(
Keyword.put_new(@pinecone_opts, :api_key, System.get_env("PINECONE_API_KEY"))
)
end
end

View file

@ -1,10 +0,0 @@
defmodule AshHq.Ashley.Registry do
@moduledoc false
use Ash.Registry,
extensions: [Ash.Registry.ResourceValidations]
entries do
entry AshHq.Ashley.Question
entry AshHq.Ashley.Conversation
end
end

View file

@ -1,94 +0,0 @@
defmodule AshHq.Ashley.Conversation do
@moduledoc false
use Ash.Resource,
data_layer: AshPostgres.DataLayer,
authorizers: [Ash.Policy.Authorizer]
@conversation_limit 10
def conversation_limit, do: @conversation_limit
actions do
defaults [:read, :destroy]
create :create do
accept [:name]
end
update :update do
accept [:name]
end
update :ask do
transaction? false
argument :question, :string do
allow_nil? false
end
manual fn changeset, _ ->
changeset.data
|> AshHq.Ashley.load!(:over_limit)
|> Map.get(:over_limit)
|> if do
Ash.Changeset.add_error(%{changeset | data: %{changeset.data | over_limit: true}},
message: "Conversation limit reached",
field: :question
)
else
AshHq.Ashley.Question.ask!(changeset.argument.question, changeset.data.id)
end
end
end
end
attributes do
uuid_primary_key :id
attribute :name, :string
end
relationships do
has_many :questions, AshHq.Ashley.Question
belongs_to :user, AshHq.Accounts.User do
api AshHq.Accounts
allow_nil? false
end
end
policies do
policy action_type(:create) do
authorize_if relating_to_actor(:user)
end
policy action_type([:read, :update, :destroy]) do
authorize_if relates_to_actor_via(:user)
end
end
postgres do
table "conversations"
repo AshHq.Repo
end
code_interface do
define_for AshHq.Ashley
define :create, args: [:name]
define :read
define :destroy
end
changes do
change relate_actor(:user), on: [:create]
end
aggregates do
count :question_count, [:questions] do
filter expr(success)
end
end
calculations do
calculate :over_limit, :boolean, expr(question_count > ^@conversation_limit)
end
end

View file

@ -1,192 +0,0 @@
defmodule AshHq.Ashley.Question.Actions.Ask do
@moduledoc false
use Ash.Resource.ManualCreate
require Logger
@dialyzer {:nowarn_function, {:create, 3}}
@dialyzer {:nowarn_function, {:sources, 1}}
@system_message_limit 4000
@static_context """
You are an assistant for helping users find relevant documentation about the Ash Framework for the programming language Elixir.
Above all else, you should provide links to relevant documentation. If you dont know the answer, do not make things up, and instead say, Sorry, Im not sure about that.
Use the following context from our documentation for your answer. All answers should be based on the documentation provided only.
Example Resource:
defmodule Post do
use Ash.Resource
actions do
defaults [:read, :update, :destroy]
create :create do
accept [:text]
change {Slugify, field: text}
end
end
attributes do
uuid_primary_key :id
attribute :text, :string, allow_nil?: false
attribute :slug, :string, allow_nil?: false
end
relationships do
belongs_to :author, User
end
end
"""
@system_message_tokens AshHq.Ashley.OpenAi.tokens(@static_context)
def create(changeset, _, %{actor: actor}) do
question = Ash.Changeset.get_attribute(changeset, :question)
{:ok, %{"data" => [%{"embedding" => vector} | _]}} =
AshHq.Ashley.OpenAi.create_embeddings([question])
{prompt, sources, system_message, system_message_tokens} =
AshHq.Ashley.Pinecone.client()
|> Pinecone.Vector.query(%{
vector: vector,
topK: 10,
includeMetadata: true,
includeValues: true
})
|> case do
{:ok,
%{
"matches" => []
}} ->
{
%{
role: :user,
content: question
},
[],
@static_context,
@system_message_tokens
}
{:ok,
%{
"matches" => matches
}} ->
# This is inefficient
context =
Enum.map_join(
matches,
"\n",
&"""
<a href="https://ash-hq.org/#{&1["metadata"]["link"]}>#{&1["metadata"]["name"]}</a>:
#{&1["metadata"]["text"]}
"""
)
system_message =
"""
#{@static_context}
#{context}
"""
|> String.slice(0..@system_message_limit)
{
%{
role: :user,
content: question
},
matches,
system_message,
AshHq.Ashley.OpenAi.tokens(system_message)
}
end
conversation_id = Ash.Changeset.get_attribute(changeset, :conversation_id)
conversation_messages =
AshHq.Ashley.Question.history!(
actor.id,
conversation_id,
query: Ash.Query.select(AshHq.Ashley.Question, [:question, :answer]),
actor: actor
)
|> Enum.flat_map(fn message ->
[
%{
role: :user,
content: message.question
},
%{
role: :assistant,
content: message.answer
}
]
end)
case AshHq.Ashley.OpenAi.complete(
system_message,
system_message_tokens,
conversation_messages ++ [prompt],
actor.email
) do
{:ok,
%{
"choices" => [
%{
"message" => %{
"content" => answer
}
}
| _
]
}} ->
answer = """
#{answer}
"""
AshHq.Ashley.Question.create(
conversation_id,
actor.id,
question,
answer,
true,
sources(sources),
authorize?: false
)
{:error, error} ->
Logger.error("""
Something went wrong creating a completion
#{Exception.message(error)}
""")
AshHq.Ashley.Question.create(
conversation_id,
actor.id,
question,
"Something went wrong",
false,
sources(sources),
authorize?: false
)
end
end
defp sources([]), do: ""
defp sources(sources) do
sources
|> Enum.map(
&%{
link: &1["metadata"]["link"],
name: &1["metadata"]["name"]
}
)
|> Enum.filter(& &1)
end
end

View file

@ -1,23 +0,0 @@
defmodule AshHq.Ashley.Question.Changes.ValidateLimit do
@moduledoc false
use Ash.Resource.Change
def change(changeset, opts, %{actor: actor}) do
Ash.Changeset.before_action(
changeset,
fn changeset ->
count =
AshHq.Ashley.Question
|> Ash.Query.for_read(:questions_in_time_frame, %{}, actor: actor)
|> AshHq.Ashley.count!()
if count >= opts[:question_limit] do
Ash.Changeset.add_error(changeset, message: "Question Quota Reached", field: :question)
else
changeset
end
end,
prepend?: true
)
end
end

View file

@ -1,154 +0,0 @@
defmodule AshHq.Ashley.Question do
@moduledoc false
use Ash.Resource,
data_layer: AshPostgres.DataLayer,
authorizers: [Ash.Policy.Authorizer],
extensions: [AshHq.Docs.Extensions.RenderMarkdown]
@time_frame_hours 24
@question_limit 10
actions do
defaults [:read, :destroy]
read :history do
argument :user, :uuid do
allow_nil? false
end
argument :conversation, :uuid do
allow_nil? false
end
prepare build(sort: [inserted_at: :asc])
filter expr(user_id == ^arg(:user) and conversation_id == ^arg(:conversation))
end
create :ask do
transaction? false
accept [:question, :conversation_id]
allow_nil_input [:conversation_id]
argument :conversation_name, :string
change fn changeset, %{actor: actor} ->
Ash.Changeset.before_action(changeset, fn changeset ->
if Ash.Changeset.get_attribute(changeset, :conversation_id) do
changeset
else
conversation =
AshHq.Ashley.Conversation.create!(
changeset.arguments[:conversation_name] || "New Conversation",
actor: actor
)
Ash.Changeset.force_change_attribute(changeset, :conversation_id, conversation.id)
end
end)
end
change fn changeset, _ ->
Ash.Changeset.timeout(changeset, :timer.minutes(3))
end
change {AshHq.Ashley.Question.Changes.ValidateLimit, limit: @question_limit}
manual AshHq.Ashley.Question.Actions.Ask
end
create :create do
accept [:conversation_id, :question, :answer, :success, :user_id, :sources]
end
read :questions_in_time_frame do
filter expr(
user_id == ^actor(:id) and inserted_at >= ago(@time_frame_hours, :hour) and success
)
end
end
attributes do
uuid_primary_key :id
attribute :question, :string do
allow_nil? false
end
attribute :answer, :string do
allow_nil? false
end
attribute :sources, {:array, AshHq.Ashley.Question.Types.Source} do
allow_nil? false
default []
end
attribute :answer_html, :string do
allow_nil? false
end
attribute :success, :boolean do
allow_nil? false
end
timestamps()
end
render_markdown do
render_attributes answer: :answer_html
end
relationships do
belongs_to :user, AshHq.Accounts.User do
allow_nil? false
api AshHq.Accounts
attribute_writable? true
end
belongs_to :conversation, AshHq.Ashley.Conversation do
allow_nil? false
attribute_writable? true
end
end
policies do
policy always() do
authorize_if actor_present()
end
policy action_type(:read) do
authorize_if action(:history)
authorize_if accessing_from(AshHq.Ashley.Conversation, :questions)
authorize_if expr(user_id == ^actor(:id))
end
policy action(:history) do
authorize_if expr(^actor(:id) == ^arg(:user))
end
policy action(:create) do
forbid_if always()
end
end
postgres do
table "questions"
repo AshHq.Repo
migration_defaults sources: "[]"
references do
reference :conversation, on_delete: :delete
end
end
code_interface do
define_for AshHq.Ashley
define :questions_in_time_frame
define :ask, args: [:question]
define :create, args: [:conversation_id, :user_id, :question, :answer, :success, :sources]
define :history, args: [:user, :conversation]
end
end

View file

@ -1,34 +0,0 @@
defmodule AshHq.Ashley.Question.Types.Source do
@moduledoc false
use Ash.Resource,
data_layer: :embedded
actions do
create :create do
primary? true
allow_nil_input [:name]
change fn changeset, _ ->
if Ash.Changeset.get_attribute(changeset, :name) do
changeset
else
Ash.Changeset.force_change_attribute(
changeset,
:name,
Ash.Changeset.get_attribute(changeset, :link)
)
end
end
end
end
attributes do
attribute :link, :string do
allow_nil? false
end
attribute :name, :string do
allow_nil? false
end
end
end

View file

@ -1,233 +0,0 @@
defmodule AshHq.Ashley.Workers.IndexLibraryVersion do
@moduledoc false
require Ash.Query
@dialyzer {:nowarn_function, {:delete_vectors, 2}}
@dialyzer {:nowarn_function, {:index_all, 0}}
@dialyzer {:nowarn_function, {:perform, 1}}
@dialyzer {:nowarn_function, {:guides, 1}}
@dialyzer {:nowarn_function, {:modules, 1}}
@dialyzer {:nowarn_function, {:functions, 1}}
@dialyzer {:nowarn_function, {:dsls, 1}}
@dialyzer {:nowarn_function, {:options, 1}}
@dialyzer {:nowarn_function, {:name, 1}}
@dialyzer {:nowarn_function, {:path, 2}}
@dialyzer {:nowarn_function, {:format, 2}}
def index_all do
AshHq.Docs.Library.read!(load: :latest_version_id)
|> Enum.filter(& &1.latest_version_id)
|> Enum.each(&perform(&1.latest_version_id))
end
def perform(id) do
pinecone_client = AshHq.Ashley.Pinecone.client()
library_version = AshHq.Docs.get!(AshHq.Docs.LibraryVersion, id, load: :library)
delete_vectors(pinecone_client, library_version)
guides(library_version)
|> Stream.concat(modules(library_version))
|> Stream.concat(functions(library_version))
|> Stream.concat(dsls(library_version))
|> Stream.concat(options(library_version))
|> Stream.map(fn item ->
{item, format(item, library_version)}
end)
|> Stream.chunk_every(100)
|> Stream.map(fn batch ->
case AshHq.Ashley.OpenAi.create_embeddings(Enum.map(batch, &elem(&1, 1))) do
{:ok, %{"data" => data}} ->
vectors =
Enum.zip_with(data, batch, fn %{"embedding" => values}, {item, text} ->
%{
values: values,
id: item.id,
metadata: %{
library: library_version.library.name,
link: "#{path(item, library_version.library.name)}",
name: "#{name(item)}",
text: text
}
}
end)
Pinecone.Vector.upsert(pinecone_client, %{vectors: vectors})
{:error, error} ->
{:error, error}
end
end)
|> Stream.run()
end
defp guides(library_version) do
AshHq.Docs.Guide
|> Ash.Query.filter(library_version_id == ^library_version.id)
|> AshHq.Docs.stream()
end
defp modules(library_version) do
AshHq.Docs.Module
|> Ash.Query.filter(library_version_id == ^library_version.id)
|> AshHq.Docs.stream()
end
defp functions(library_version) do
AshHq.Docs.Function
|> Ash.Query.filter(library_version_id == ^library_version.id)
|> Ash.Query.load(:module_name)
|> AshHq.Docs.stream()
end
defp dsls(library_version) do
AshHq.Docs.Dsl
|> Ash.Query.filter(library_version_id == ^library_version.id)
|> Ash.Query.load(:extension_target)
|> AshHq.Docs.stream()
end
defp options(library_version) do
AshHq.Docs.Option
|> Ash.Query.filter(library_version_id == ^library_version.id)
|> Ash.Query.load(:extension_target)
|> AshHq.Docs.stream()
end
defp name(%AshHq.Docs.Option{} = option) do
case option.path do
[] ->
"#{option.extension_target} - #{option.name}"
path ->
"#{option.extension_target} - #{Enum.join(path, ".")} | #{option.name}"
end
end
defp name(%AshHq.Docs.Dsl{} = dsl) do
case dsl.path do
[] ->
"#{dsl.extension_target} - #{dsl.name}"
path ->
"#{dsl.extension_target} - #{Enum.join(path ++ [dsl.name], ".")}"
end
end
defp name(%AshHq.Docs.Function{} = function) do
"#{function.type} - #{function.module_name}.#{function.name}/#{function.arity}"
end
defp name(%AshHq.Docs.Module{} = module) do
module.name
end
defp name(%AshHq.Docs.Guide{} = guide) do
guide.name
end
defp path(%AshHq.Docs.Option{} = option, _library_name) do
"docs/dsl/#{sanitize_name(option.extension_target)}##{String.replace(option.sanitized_path, "/", "-")}-#{sanitize_name(option.name)}"
end
defp path(%AshHq.Docs.Dsl{} = option, _library_name) do
"docs/dsl/#{sanitize_name(option.extension_target)}##{String.replace(option.sanitized_path, "/", "-")}"
end
defp path(
%AshHq.Docs.Function{
sanitized_name: sanitized_name,
arity: arity,
type: type,
module_name: module_name
},
library_name
) do
"/docs/module/#{library_name}/latest/#{sanitize_name(module_name)}##{type}-#{sanitized_name}-#{arity}"
end
defp path(
%AshHq.Docs.Module{
sanitized_name: sanitized_name
},
library_name
) do
"/docs/module/#{library_name}/latest/#{sanitized_name}"
end
defp path(
%AshHq.Docs.Guide{
route: route
},
library_name
) do
"/docs/guides/#{library_name}/latest/#{route}"
end
defp format(%AshHq.Docs.Option{} = option, _library_version) do
"""
DSL Entity: #{Enum.join(option.path ++ [option.name])}
Type: #{option.type}
Default: #{option.default}
#{option.doc}
"""
end
defp format(%AshHq.Docs.Dsl{type: :entity} = dsl, _library_version) do
"""
DSL Entity: #{Enum.join(dsl.path ++ [dsl.name])}
#{Enum.map_join(dsl.examples || [], &"```\n#{&1}\n```")}
#{dsl.doc}
"""
end
defp format(%AshHq.Docs.Dsl{type: :section} = dsl, _library_version) do
"""
DSL Section: #{Enum.join(dsl.path ++ [dsl.name])}
#{Enum.map_join(dsl.examples || [], &"```\n#{&1}\n```")}
#{dsl.doc}
"""
end
defp format(%AshHq.Docs.Function{} = function, _library_version) do
"""
#{function.type} #{function.module_name}.#{function.name}/#{function.arity}
Types:
#{Enum.join(function.heads, "\n")}
Docs:
#{function.doc}
"""
end
defp format(%AshHq.Docs.Module{} = module, _library_version) do
"""
#{module.name}:
#{module.doc}
"""
end
defp format(%AshHq.Docs.Guide{} = guide, _library_version) do
"""
#{guide.name}:
#{guide.text}
"""
end
def sanitize_name(name, allow_forward_slash? \\ false) do
if allow_forward_slash? do
String.downcase(String.replace(to_string(name), ~r/[^A-Za-z0-9\/_]/, "-"))
else
String.downcase(String.replace(to_string(name), ~r/[^A-Za-z0-9_]/, "-"))
end
end
defp delete_vectors(pinecone_client, library_version) do
pinecone_client
|> Pinecone.Vector.delete(%{
filter: %{
library: library_version.library.name
}
})
end
end

View file

@ -1,8 +0,0 @@
defmodule AshHq.Discord do
@moduledoc "Discord api import & interactions"
use Ash.Api
resources do
registry(AshHq.Discord.Registry)
end
end

View file

@ -5,99 +5,33 @@ defmodule AshHq.Discord.Listener do
use Nostrum.Consumer
import Bitwise
@all_types AshHq.Docs.Extensions.Search.Types.types() -- ["Forum"]
@user_id 1_066_406_803_769_933_834
@server_id 711_271_361_523_351_632
def start_link() do
Consumer.start_link(__MODULE__)
end
def search_results!(interaction) do
search =
interaction.data.options
|> Enum.find_value(fn option ->
if option.name == "search" do
option.value
end
end)
item_list = AshHq.Docs.Indexer.search!(search)
type =
interaction.data.options
|> Enum.find_value(fn option ->
if option.name == "type" do
option.value
end
end)
item_list = Enum.take(item_list, 10)
library =
interaction.data.options
|> Enum.find_value(fn option ->
if option.name == "library" do
option.value
end
end)
count =
case Enum.count(item_list) do
10 ->
"the top 10"
libraries =
AshHq.Docs.Library.read!()
|> Enum.filter(& &1.latest_version_id)
library_version_ids =
if library do
case Enum.find(libraries, &(&1.name == library)) do
nil ->
[]
library ->
[library.latest_version_id]
end
else
Enum.map(libraries, & &1.latest_version_id)
other ->
"#{other}"
end
input =
if type do
%{types: [type]}
else
%{types: @all_types}
end
"""
Found #{count} results for "#{search}":
%{result: item_list} = AshHq.Docs.Search.run!(search, library_version_ids, input)
result_type =
if type do
"#{type} results"
else
"results"
end
library =
if library do
"#{library}"
else
"all libraries"
end
if item_list do
item_list = Enum.take(item_list, 10)
count =
case Enum.count(item_list) do
10 ->
"the top 10"
other ->
"#{other}"
end
"""
Found #{count} #{result_type} in #{library} for query "#{search}":
#{Enum.map_join(item_list, "\n", &render_search_result(&1))}
"""
else
"Something went wrong."
end
#{Enum.map_join(item_list, "\n", &render_search_result(&1))}
"""
end
defp render_search_result(item) do
@ -161,19 +95,11 @@ defmodule AshHq.Discord.Listener do
def rebuild do
if Application.get_env(:ash_hq, :discord_bot) do
libraries =
AshHq.Docs.Library.read!()
|> Enum.filter(& &1.latest_library_version)
build_search_action(libraries)
build_search_action()
end
end
defp build_search_action(libraries) do
library_names =
libraries
|> Enum.map(& &1.name)
defp build_search_action() do
command = %{
name: "ash_hq_search",
description: "Search AshHq Documentation",
@ -185,36 +111,6 @@ defmodule AshHq.Discord.Listener do
description: "what you want to search for",
required: true
},
%{
# ApplicationCommandType::STRING
type: 3,
name: "type",
description: "What type of thing you want to search for. Defaults to everything.",
required: false,
choices:
Enum.map(@all_types, fn type ->
%{
name: String.downcase(type),
description: "Search only for #{String.downcase(type)} items.",
value: type
}
end)
},
%{
# ApplicationCommandType::STRING
type: 3,
name: "library",
description: "Which library you'd like to search. Defaults to all libraries.",
required: false,
choices:
Enum.map(library_names, fn name ->
%{
name: name,
description: "Search only in the #{name} library.",
value: name
}
end)
},
%{
# ApplicationCommandType::Boolean
type: 5,
@ -227,7 +123,7 @@ defmodule AshHq.Discord.Listener do
Nostrum.Api.create_guild_application_command(
@user_id,
AshHq.Discord.Poller.server_id(),
@server_id,
command
)
end

View file

@ -1,183 +0,0 @@
defmodule AshHq.Discord.Poller do
@moduledoc """
Every 2 hours, synchronizes all active threads and the 50 most recent archived threads
"""
use GenServer
require Logger
@poll_interval :timer.hours(1)
@server_id 711_271_361_523_351_632
@archived_thread_lookback 50
@channels [
1_066_222_835_758_014_606,
1_066_223_107_922_210_867,
1_019_647_368_196_534_283
]
def server_id, do: @server_id
defmacrop unwrap(value) do
quote do
case unquote(value) do
{:ok, value} ->
value
{:error, error} ->
raise Exception.format(:error, error, [])
end
end
end
def start_link(state) do
GenServer.start_link(__MODULE__, state, name: __MODULE__)
end
def init(_) do
send(self(), :poll)
{:ok, nil}
end
def handle_info(:poll, state) do
poll()
Process.send_after(self(), :poll, @poll_interval)
{:noreply, state}
end
def poll do
for {channel, index} <- Enum.with_index(@channels) do
channel
|> Nostrum.Api.get_channel!()
|> tap(fn channel ->
channel
|> Map.from_struct()
|> Map.put(:order, index)
|> AshHq.Discord.Channel.upsert!()
end)
|> Map.get(:available_tags)
|> Enum.each(fn available_tag ->
AshHq.Discord.Tag.upsert!(channel, available_tag.id, available_tag.name)
end)
end
active =
@server_id
|> Nostrum.Api.list_guild_threads()
|> unwrap()
|> Map.get(:threads)
|> Stream.filter(fn thread ->
thread.parent_id in @channels
end)
|> Stream.map(fn thread ->
%{
thread: thread,
messages: get_all_channel_messages(thread.id)
}
end)
archived =
@channels
|> Stream.flat_map(fn channel ->
channel
|> Nostrum.Api.list_public_archived_threads(limit: @archived_thread_lookback)
|> unwrap()
|> Map.get(:threads)
|> Enum.map(fn thread ->
messages =
thread.id
|> get_all_channel_messages()
%{
thread: thread,
messages: messages
}
end)
end)
active
|> Stream.concat(archived)
|> Enum.reject(fn
%{messages: []} ->
true
_ ->
false
end)
|> Enum.each(fn %{thread: thread, messages: messages} ->
try do
author =
messages
|> Enum.min_by(& &1.timestamp, DateTime)
|> Map.get(:author)
thread
|> Map.put(:author, author)
|> Map.from_struct()
|> Map.put(:channel_id, thread.parent_id)
|> Map.put(:tags, thread.applied_tags)
|> Map.put(:create_timestamp, thread.thread_metadata.create_timestamp)
|> Map.put(:messages, Enum.map(messages, &Map.from_struct/1))
|> AshHq.Discord.Thread.upsert!()
rescue
e ->
Logger.error(
"Failed to import message:\n #{Exception.format(:error, e, __STACKTRACE__)}"
)
end
end)
end
defp get_all_channel_messages(thread) do
Stream.resource(
fn ->
:all
end,
fn
nil ->
{:halt, nil}
before ->
locator =
case before do
:all ->
nil
before ->
{:before, before}
end
messages =
if locator do
Nostrum.Api.get_channel_messages!(thread, 100, locator)
else
Nostrum.Api.get_channel_messages!(thread, 100)
end
if Enum.count(messages) == 100 do
{messages, List.last(messages).id}
else
{messages, nil}
end
end,
& &1
)
|> Stream.map(fn message ->
message
|> Map.put(:author, message.author.username)
|> Map.update!(:reactions, fn reactions ->
reactions
|> Kernel.||([])
# just don't know what this looks like, so removing them
|> Enum.reject(&(is_nil(&1.emoji) || &1.emoji == "" || &1.emoji.animated))
|> Enum.map(fn %{count: count, emoji: emoji} ->
%{emoji: emoji.name, count: count}
end)
end)
|> Map.update!(:attachments, fn attachments ->
Enum.map(attachments, &Map.from_struct/1)
end)
end)
|> Enum.to_list()
end
end

View file

@ -1,15 +0,0 @@
defmodule AshHq.Discord.Registry do
@moduledoc false
use Ash.Registry,
extensions: [Ash.Registry.ResourceValidations]
entries do
entry AshHq.Discord.Attachment
entry AshHq.Discord.Channel
entry AshHq.Discord.Message
entry AshHq.Discord.Reaction
entry AshHq.Discord.Tag
entry AshHq.Discord.Thread
entry AshHq.Discord.ThreadTag
end
end

View file

@ -1,35 +0,0 @@
defmodule AshHq.Discord.Attachment do
@moduledoc "A discord attachment on a message"
use Ash.Resource,
data_layer: AshPostgres.DataLayer
actions do
defaults [:create, :read, :update, :destroy]
end
attributes do
integer_primary_key :id, generated?: false, writable?: true
attribute :filename, :string
attribute :size, :integer
attribute :url, :string
attribute :proxy_url, :string
attribute :height, :integer
attribute :width, :integer
end
relationships do
belongs_to :message, AshHq.Discord.Message do
allow_nil? false
attribute_type :integer
end
end
postgres do
table "discord_attachments"
repo AshHq.Repo
references do
reference :message, on_delete: :delete, on_update: :update
end
end
end

View file

@ -1,43 +0,0 @@
defmodule AshHq.Discord.Channel do
@moduledoc """
The channel is the discord forum channel. We explicitly configure which ones we import.
"""
use Ash.Resource,
data_layer: AshPostgres.DataLayer
actions do
defaults [:create, :read, :update, :destroy]
create :upsert do
upsert? true
end
end
attributes do
integer_primary_key :id, writable?: true, generated?: false
attribute :name, :string do
allow_nil? false
end
attribute :order, :integer do
allow_nil? false
end
end
relationships do
has_many :threads, AshHq.Discord.Thread
end
postgres do
table "discord_channels"
repo AshHq.Repo
end
code_interface do
define_for AshHq.Discord
define :read
define :upsert
end
end

View file

@ -1,96 +0,0 @@
defmodule AshHq.Discord.Message do
@moduledoc """
Discord messages synchronized by the discord bot
"""
use Ash.Resource,
data_layer: AshPostgres.DataLayer,
extensions: [
AshHq.Docs.Extensions.RenderMarkdown,
AshHq.Docs.Extensions.Search
]
actions do
defaults [:read, :destroy]
create :create do
primary? true
argument :attachments, {:array, :map}
argument :reactions, {:array, :map}
change manage_relationship(:attachments, type: :direct_control)
change manage_relationship(:reactions,
type: :direct_control,
use_identities: [:unique_message_emoji]
)
end
update :update do
primary? true
argument :attachments, {:array, :map}
argument :reactions, {:array, :map}
change manage_relationship(:attachments, type: :direct_control)
change manage_relationship(:reactions,
type: :direct_control,
use_identities: [:unique_message_emoji]
)
end
end
render_markdown do
render_attributes content: :content_html
end
search do
doc_attribute :content
type "Forum"
load_for_search [
:channel_name,
:thread_name
]
has_name_attribute? false
weight_content(-0.7)
end
attributes do
integer_primary_key :id, generated?: false, writable?: true
attribute :author, :string do
allow_nil? false
end
attribute :content, :string
attribute :content_html, :string
attribute :timestamp, :utc_datetime do
allow_nil? false
end
end
relationships do
belongs_to :thread, AshHq.Discord.Thread do
attribute_type :integer
allow_nil? false
end
has_many :attachments, AshHq.Discord.Attachment
has_many :reactions, AshHq.Discord.Reaction
end
postgres do
table "discord_messages"
repo AshHq.Repo
references do
reference :thread, on_delete: :delete, on_update: :update
end
end
aggregates do
first :channel_name, [:thread, :channel], :name
first :thread_name, [:thread], :name
end
end

View file

@ -1,43 +0,0 @@
defmodule AshHq.Discord.Reaction do
@moduledoc """
Reactions store emoji reaction counts.
"""
use Ash.Resource,
data_layer: AshPostgres.DataLayer
actions do
defaults [:create, :read, :update, :destroy]
end
attributes do
uuid_primary_key :id
attribute :count, :integer do
allow_nil? false
end
attribute :emoji, :string do
allow_nil? false
end
end
relationships do
belongs_to :message, AshHq.Discord.Message do
attribute_type :integer
allow_nil? false
end
end
postgres do
table "discord_reactions"
repo AshHq.Repo
references do
reference :message, on_delete: :delete, on_update: :update
end
end
identities do
identity :unique_message_emoji, [:emoji, :message_id]
end
end

View file

@ -1,45 +0,0 @@
defmodule AshHq.Discord.Tag do
@moduledoc "A tag that can be applied to a post. Currently uses CSV data layer and therefore is static"
use Ash.Resource,
data_layer: AshPostgres.DataLayer
actions do
defaults [:create, :read, :update, :destroy]
create :upsert do
upsert? true
upsert_identity :unique_name_per_channel
end
end
attributes do
integer_primary_key :id, generated?: false, writable?: true
attribute :name, :ci_string do
allow_nil? false
end
end
relationships do
belongs_to :channel, AshHq.Discord.Channel do
attribute_type :integer
attribute_writable? true
end
end
postgres do
table "discord_tags"
repo AshHq.Repo
end
code_interface do
define_for AshHq.Discord
define :upsert, args: [:channel_id, :id, :name]
define :read
define :destroy
end
identities do
identity :unique_name_per_channel, [:name, :channel_id]
end
end

View file

@ -1,109 +0,0 @@
defmodule AshHq.Discord.Thread do
@moduledoc """
A thread is an individual forum post (because they are really just fancy threads)
"""
use Ash.Resource,
data_layer: AshPostgres.DataLayer
import Ecto.Query
actions do
defaults [:create, :read, :update, :destroy]
read :feed do
pagination do
countable true
offset? true
default_limit 25
end
argument :channel, :integer do
allow_nil? false
end
argument :tag_name, :string
prepare build(sort: [create_timestamp: :desc])
filter expr(
channel_id == ^arg(:channel) and
(is_nil(^arg(:tag_name)) or tags.name == ^arg(:tag_name))
)
end
create :upsert do
upsert? true
argument :messages, {:array, :map}
argument :tags, {:array, :integer}
change manage_relationship(:messages, type: :direct_control)
change fn changeset, _ ->
Ash.Changeset.after_action(changeset, fn changeset, thread ->
tags = Ash.Changeset.get_argument(changeset, :tags) || []
# Not optimized in `manage_relationship`
# bulk actions should make this unnecessary
to_delete =
from thread_tag in AshHq.Discord.ThreadTag,
where: thread_tag.thread_id == ^thread.id,
where: thread_tag.tag_id not in ^tags
AshHq.Repo.delete_all(to_delete)
Enum.map(tags, fn tag ->
AshHq.Discord.ThreadTag.tag!(thread.id, tag)
end)
{:ok, thread}
end)
end
end
end
attributes do
integer_primary_key :id, generated?: false, writable?: true
attribute :type, :integer
attribute :name, :string do
allow_nil? false
end
attribute :author, :string do
allow_nil? false
end
attribute :create_timestamp, :utc_datetime do
allow_nil? false
end
end
relationships do
has_many :messages, AshHq.Discord.Message
belongs_to :channel, AshHq.Discord.Channel do
attribute_type :integer
allow_nil? false
attribute_writable? true
end
many_to_many :tags, AshHq.Discord.Tag do
through AshHq.Discord.ThreadTag
source_attribute_on_join_resource :thread_id
destination_attribute_on_join_resource :tag_id
end
end
postgres do
table "discord_threads"
repo AshHq.Repo
end
code_interface do
define_for AshHq.Discord
define :upsert
define :by_id, action: :read, get_by: [:id]
define :feed, args: [:channel]
end
end

View file

@ -1,39 +0,0 @@
defmodule AshHq.Discord.ThreadTag do
@moduledoc "Joins a thread to a tag"
use Ash.Resource,
data_layer: AshPostgres.DataLayer
actions do
defaults [:read, :destroy]
create :tag do
upsert? true
end
end
relationships do
belongs_to :thread, AshHq.Discord.Thread do
primary_key? true
allow_nil? false
attribute_writable? true
attribute_type :integer
end
belongs_to :tag, AshHq.Discord.Tag do
primary_key? true
allow_nil? false
attribute_writable? true
attribute_type :integer
end
end
postgres do
table "discord_thread_tags"
repo AshHq.Repo
end
code_interface do
define_for AshHq.Discord
define :tag, args: [:thread_id, :tag_id]
end
end

View file

@ -0,0 +1 @@

View file

@ -1,21 +0,0 @@
defmodule AshHq.Extensions.Search.Preparations.LoadSearchData do
@moduledoc """
Ensures that any data needed for search results is loaded.
"""
use Ash.Resource.Preparation
def prepare(query, _, _) do
query_string = Ash.Query.get_argument(query, :query)
to_load = AshHq.Docs.Extensions.Search.load_for_search(query.resource)
query.resource
|> AshHq.Docs.Extensions.RenderMarkdown.render_attributes()
|> Enum.reduce(query, fn {source, target}, query ->
Ash.Query.deselect(query, [source, target])
end)
|> Ash.Query.load(search_headline: [query: query_string])
|> Ash.Query.load(match_rank: [query: query_string])
|> Ash.Query.load(to_load)
|> Ash.Query.sort(match_rank: {:asc, %{query: query_string}})
end
end

View file

@ -60,11 +60,6 @@ defmodule AshHq.Docs.Extensions.Search do
type: :atom,
doc: "The text field to be used in the search"
],
library_version_attribute: [
type: :atom,
default: :library_version_id,
doc: "The attribute to use to filter by library version"
],
load_for_search: [
type: {:list, :any},
default: [],
@ -140,10 +135,6 @@ defmodule AshHq.Docs.Extensions.Search do
|> List.wrap()
end
def library_version_attribute(resource) do
Extension.get_opt(resource, [:search], :library_version_attribute, :library_version_id)
end
def load_for_search(resource) do
Extension.get_opt(resource, [:search], :load_for_search, :library_version_id)
end

View file

@ -4,15 +4,8 @@ defmodule AshHq.Docs.Extensions.Search.Transformers.AddSearchStructure do
* Adds a sanitized name attribute if it doesn't already exist
* Adds a change to set the sanitized name, if it should.
* Adds a `search_headline` calculation
* Adds a `matches` calculation
* Adds relevant indexes using custom sql statements
* Adds a `match_rank` calculation.
* Adds a search action
* Adds a code interface for the search action
"""
use Spark.Dsl.Transformer
import Ash.Filter.TemplateHelpers
require Ash.Query
alias Spark.Dsl.Transformer
@ -26,36 +19,10 @@ defmodule AshHq.Docs.Extensions.Search.Transformers.AddSearchStructure do
config = %{
name_attribute: name_attribute,
doc_attribute: Transformer.get_option(dsl_state, [:search], :doc_attribute),
library_version_attribute:
Transformer.get_option(dsl_state, [:search], :library_version_attribute) ||
:library_version_id,
table: Transformer.get_option(dsl_state, [:postgres], :table),
sanitized_name_attribute: sanitized_name_attribute
}
currently_ignored_attributes =
AshPostgres.DataLayer.Info.migration_ignore_attributes(dsl_state)
dsl_state
|> add_sanitized_name(config)
|> add_search_action(config)
|> add_code_interface()
|> Transformer.set_option([:postgres], :migration_ignore_attributes, [
:searchable | currently_ignored_attributes
])
|> add_search_headline_calculation(config)
|> add_matches_calculation()
|> add_full_text_column(config)
|> add_full_text_index()
|> add_match_rank_calculation(config)
|> Ash.Resource.Builder.add_preparation(
{AshHq.Docs.Extensions.Search.Preparations.DeselectSearchable, []}
)
|> Ash.Resource.Builder.add_attribute(:searchable, AshHq.Docs.Search.Types.TsVector,
generated?: true,
private?: true
)
{:ok, add_sanitized_name(dsl_state, config)}
end
defp add_sanitized_name(dsl_state, config) do
@ -104,253 +71,6 @@ defmodule AshHq.Docs.Extensions.Search.Transformers.AddSearchStructure do
end
end
defp add_full_text_index(dsl_state) do
Transformer.add_entity(
dsl_state,
[:postgres, :custom_indexes],
Transformer.build_entity!(
AshPostgres.DataLayer,
[:postgres, :custom_indexes],
:index,
fields: [:searchable],
using: "GIN"
)
)
end
defp add_full_text_column(dsl_state, config) do
if config.doc_attribute do
if Transformer.get_option(dsl_state, [:search], :has_name_attribute?, true) do
Transformer.add_entity(
dsl_state,
[:postgres, :custom_statements],
Transformer.build_entity!(
AshPostgres.DataLayer,
[:postgres, :custom_statements],
:statement,
name: :search_column,
up: """
ALTER TABLE #{config.table}
ADD COLUMN searchable tsvector
GENERATED ALWAYS AS (
setweight(to_tsvector('english', #{config.name_attribute}), 'A') ||
setweight(to_tsvector('english', #{config.doc_attribute}), 'D')
) STORED;
""",
down: """
ALTER TABLE #{config.table}
DROP COLUMN searchable
"""
)
)
else
Transformer.add_entity(
dsl_state,
[:postgres, :custom_statements],
Transformer.build_entity!(
AshPostgres.DataLayer,
[:postgres, :custom_statements],
:statement,
name: :search_column,
up: """
ALTER TABLE #{config.table}
ADD COLUMN searchable tsvector
GENERATED ALWAYS AS (
setweight(to_tsvector('english', #{config.doc_attribute}), 'D')
) STORED;
""",
down: """
ALTER TABLE #{config.table}
DROP COLUMN searchable
"""
)
)
end
else
Transformer.add_entity(
dsl_state,
[:postgres, :custom_statements],
Transformer.build_entity!(
AshPostgres.DataLayer,
[:postgres, :custom_statements],
:statement,
name: :search_column,
up: """
ALTER TABLE #{config.table}
ADD COLUMN searchable tsvector
GENERATED ALWAYS AS (
setweight(to_tsvector('english', #{config.name_attribute}), 'A')
) STORED;
""",
down: """
ALTER TABLE #{config.table}
DROP COLUMN searchable
"""
)
)
end
end
defp add_match_rank_calculation(dsl_state, _config) do
weight_content = AshHq.Docs.Extensions.Search.weight_content(dsl_state)
dsl_state
|> Transformer.add_entity(
[:calculations],
Transformer.build_entity!(Ash.Resource.Dsl, [:calculations], :calculate,
name: :match_rank,
type: :float,
private?: true,
arguments: [query_argument()],
calculation:
Ash.Query.expr(
fragment(
"(ts_rank_cd('{0.05, 0.1, 0.1, 1.0}', ?, websearch_to_tsquery(?), 32) + ?)",
^ref(:searchable),
^arg(:query),
^weight_content
)
)
)
)
end
defp add_matches_calculation(dsl_state) do
Transformer.add_entity(
dsl_state,
[:calculations],
Transformer.build_entity!(Ash.Resource.Dsl, [:calculations], :calculate,
name: :matches,
type: :boolean,
private?: true,
arguments: [query_argument()],
calculation:
Ash.Query.expr(
fragment(
"(? @@ websearch_to_tsquery(?))",
^ref(:searchable),
^arg(:query)
)
)
)
)
end
# defp add_name_matches_calculation(dsl_state, config) do
# if AshHq.Docs.Extensions.Search.has_name_attribute?(dsl_state) do
# Transformer.add_entity(
# dsl_state,
# [:calculations],
# Transformer.build_entity!(Ash.Resource.Dsl, [:calculations], :calculate,
# name: :name_matches,
# type: :boolean,
# arguments: [query_argument(), similarity_argument()],
# private?: true,
# calculation:
# Ash.Query.expr(
# contains(
# fragment("lower(?)", ^ref(config.name_attribute)),
# fragment("lower(?)", ^arg(:query))
# )
# )
# )
# )
# else
# dsl_state
# end
# end
defp add_search_headline_calculation(dsl_state, config) do
if config.doc_attribute do
Transformer.add_entity(
dsl_state,
[:calculations],
Transformer.build_entity!(Ash.Resource.Dsl, [:calculations], :calculate,
name: :search_headline,
type: :string,
private?: true,
arguments: [query_argument()],
calculation:
Ash.Query.expr(
# credo:disable-for-next-line
fragment(
"ts_headline('english', ?, websearch_to_tsquery('english', ?), 'MaxFragments=2,StartSel=\"<span class=\"\"search-hit\"\">\", StopSel=</span>')",
^ref(config.doc_attribute),
^arg(:query)
)
)
)
)
else
Transformer.add_entity(
dsl_state,
[:calculations],
Transformer.build_entity!(Ash.Resource.Dsl, [:calculations], :calculate,
name: :search_headline,
type: :string,
arguments: [query_argument()],
private?: true,
calculation: Ash.Query.expr("")
)
)
end
end
defp query_argument do
Transformer.build_entity!(
Ash.Resource.Dsl,
[:calculations, :calculate],
:argument,
type: :string,
name: :query,
allow_nil?: false
)
end
defp add_search_action(dsl_state, _config) do
query_argument =
Transformer.build_entity!(
Ash.Resource.Dsl,
[:actions, :read],
:argument,
type: :string,
name: :query
)
{arguments, filter} =
{[query_argument], Ash.Query.expr(matches(query: arg(:query)))}
Transformer.add_entity(
dsl_state,
[:actions],
Transformer.build_entity!(Ash.Resource.Dsl, [:actions], :read,
name: :search,
arguments: arguments,
preparations: search_preparations(),
filter: filter
)
)
end
defp add_code_interface(dsl_state) do
Transformer.add_entity(
dsl_state,
[:code_interface],
Transformer.build_entity!(Ash.Resource.Dsl, [:code_interface], :define,
name: :search,
args: [:query]
)
)
end
defp search_preparations do
[
Transformer.build_entity!(Ash.Resource.Dsl, [:actions, :read], :prepare,
preparation: AshHq.Extensions.Search.Preparations.LoadSearchData
)
]
end
def before?(Ash.Resource.Transformers.SetTypes), do: true
def before?(_), do: false
def after?(Ash.Resource.Transformers.SetPrimaryActions), do: true

View file

@ -3,7 +3,7 @@ defmodule AshHq.Docs.Extensions.Search.Types do
A static list of all search types that currently exist
"""
@search_types [AshHq.Docs.Registry, AshHq.Discord.Registry]
@search_types [AshHq.Docs.Registry]
|> Enum.flat_map(&Ash.Registry.Info.entries/1)
|> Enum.filter(&(AshHq.Docs.Extensions.Search in Spark.extensions(&1)))
|> Enum.map(&AshHq.Docs.Extensions.Search.type/1)

View file

@ -1,11 +0,0 @@
defmodule AshHq.Docs.Search.Types.TsVector do
@moduledoc "A stub for a tsvector type that should never actually get loaded."
use Ash.Type
def storage_type, do: :tsvector
def cast_in_query?(_), do: false
defdelegate cast_input(value, constraints), to: Ash.Type.String
defdelegate cast_stored(value, constraints), to: Ash.Type.String
defdelegate dump_to_native(value, constraints), to: Ash.Type.String
end

View file

@ -135,8 +135,6 @@ defmodule AshHq.Docs.Importer do
Logger.error(
"Failed to import version #{name} #{version} #{Exception.format(:error, e, __STACKTRACE__)}"
)
e
end
end)
end
@ -219,34 +217,21 @@ defmodule AshHq.Docs.Importer do
end
if result do
{:ok, library_version} =
AshHq.Repo.transaction(fn ->
Logger.info("Starting import of #{name}: #{version}")
Logger.info("Starting import of #{name}: #{version}")
id =
case LibraryVersion.by_version(library.id, version) do
{:ok, version} ->
LibraryVersion.destroy!(version)
version.id
_ ->
Ash.UUID.generate()
end
LibraryVersion.build!(
library.id,
version,
%{
timeout: :infinity,
id: id,
extensions: result[:extensions],
doc: result[:doc],
guides: result[:guides],
modules: result[:modules],
mix_tasks: result[:mix_tasks]
}
)
end)
library_version =
LibraryVersion.build!(
library.id,
version,
%{
timeout: :infinity,
extensions: result[:extensions],
doc: result[:doc],
guides: result[:guides],
modules: result[:modules],
mix_tasks: result[:mix_tasks]
}
)
LibraryVersion
|> Ash.Query.for_read(:read)

214
lib/ash_hq/docs/indexer.ex Normal file
View file

@ -0,0 +1,214 @@
defmodule AshHq.Docs.Indexer do
use GenServer
require Ash.Query
def start_link(state, opts \\ []) do
GenServer.start_link(__MODULE__, state, opts)
end
def init(_) do
{:ok, %{haystack: haystack()}, {:continue, :index}}
end
def haystack do
Haystack.index(Haystack.new(), :search, fn index ->
index
|> Haystack.Index.ref(Haystack.Index.Field.term("id"))
|> Haystack.Index.field(Haystack.Index.Field.new("name"))
|> Haystack.Index.field(Haystack.Index.Field.new("call_name"))
|> Haystack.Index.field(Haystack.Index.Field.new("doc"))
|> Haystack.Index.field(Haystack.Index.Field.new("library_name"))
|> Haystack.Index.storage(storage())
end)
end
def storage do
Haystack.Storage.ETS.new(name: :search, table: :search)
end
def search(search) do
tokens = Haystack.Tokenizer.tokenize(search)
tokens = Haystack.Transformer.pipeline(tokens, Haystack.Transformer.default())
Haystack.index(haystack(), :search, fn index ->
query =
Enum.reduce(tokens, Haystack.Query.Clause.new(:any), fn token, clause ->
Enum.reduce(
Map.values(Map.take(index.fields, ["name", "call_name", "description"])),
clause,
fn field, clause ->
Haystack.Query.Clause.expressions(clause, [
Haystack.Query.Expression.new(:match, field: field.k, term: token.v)
])
end
)
end)
Haystack.Query.new()
|> Haystack.Query.clause(query)
|> Haystack.Query.run(index)
end)
|> Stream.map(fn item ->
[type, id] = String.split(item.ref, "|")
%{id: id, type: type, score: item.score}
end)
|> Enum.group_by(& &1.type)
|> Enum.flat_map(fn {type, items} ->
resource =
case type do
"dsl" -> AshHq.Docs.Dsl
"guide" -> AshHq.Docs.Guide
"option" -> AshHq.Docs.Option
"module" -> AshHq.Docs.Module
"mix_task" -> AshHq.Docs.MixTask
"function" -> AshHq.Docs.Function
end
ids = Enum.map(items, & &1.id)
scores = Map.new(items, &{&1.id, &1.score})
resource
|> Ash.Query.filter(id in ^ids)
|> Ash.Query.load(AshHq.Docs.Extensions.Search.load_for_search(resource))
|> AshHq.Docs.read!()
|> Enum.map(fn item ->
Ash.Resource.put_metadata(item, :search_score, scores[item.id])
end)
end)
|> Enum.sort_by(&{!exact_match?(&1, search), -&1.__metadata__.search_score})
end
defp exact_match?(record, search) do
record
|> Map.take([:name, :call_name])
|> Map.values()
|> Enum.any?(fn value ->
is_binary(value) &&
String.downcase(value) == String.downcase(String.trim_trailing(search, "("))
end)
end
def handle_continue(:index, state) do
{:noreply, index(state)}
end
def handle_info(:index, state) do
{:noreply, index(state)}
end
def handle_info(_, state), do: {:noreply, state}
defp index(state) do
haystack =
Haystack.index(state.haystack, :search, fn index ->
[
dsls(),
guides(),
options(),
modules(),
mix_tasks(),
functions()
]
|> Stream.concat()
|> Stream.chunk_every(100)
|> Enum.each(&Haystack.Index.add(index, &1))
end)
%{state | haystack: haystack}
after
Process.send_after(self(), :index, :timer.hours(6))
end
defp dsls do
AshHq.Docs.Dsl
|> Ash.Query.load([:library_name, :extension_module])
|> AshHq.Docs.stream!()
|> Stream.map(fn dsl ->
%{
"id" => id("dsl", dsl.id),
"name" => dsl.name,
"library_name" => dsl.library_name,
"doc" => dsl.doc,
"call_name" => "#{dsl.extension_module}.#{dsl.sanitized_path}.#{dsl.name}"
}
end)
end
defp guides do
AshHq.Docs.Guide
|> Ash.Query.load(library_version: :library)
|> AshHq.Docs.stream!()
|> Stream.map(fn guide ->
%{
"id" => id("guide", guide.id),
"name" => guide.name,
"library_name" => guide.library_version.library.name,
"doc" => guide.text
}
end)
end
defp options do
AshHq.Docs.Option
|> Ash.Query.load([:library_name, :extension_module])
|> AshHq.Docs.stream!()
|> Stream.map(fn option ->
%{
"id" => id("option", option.id),
"name" => option.name,
"library_name" => option.library_name,
"doc" => option.doc,
"call_name" => "#{option.extension_module}.#{option.sanitized_path}.#{option.name}"
}
end)
end
defp modules do
AshHq.Docs.Module
|> Ash.Query.load([:library_name])
|> AshHq.Docs.stream!()
|> Stream.map(fn module ->
%{
"id" => id("module", module.id),
"name" => module.name,
"library_name" => module.library_name,
"doc" => module.doc,
"call_name" => module.name
}
end)
end
defp mix_tasks do
AshHq.Docs.MixTask
|> Ash.Query.load([:library_name])
|> AshHq.Docs.stream!()
|> Stream.map(fn mix_task ->
%{
"id" => id("mix_task", mix_task.id),
"name" => mix_task.module_name,
"library_name" => mix_task.library_name,
"doc" => mix_task.doc,
"call_name" => mix_task.name
}
end)
end
defp functions do
AshHq.Docs.Function
|> Ash.Query.load([:library_name, :call_name])
|> AshHq.Docs.stream!()
|> Stream.map(fn function ->
%{
"id" => id("function", function.id),
"name" => function.name,
"library_name" => function.library_name,
"doc" => function.doc,
"call_name" => function.call_name
}
end)
end
defp id(type, id), do: "#{type}|#{id}"
end

View file

@ -2,9 +2,41 @@ defmodule AshHq.Docs.Dsl do
@moduledoc false
use Ash.Resource,
data_layer: AshPostgres.DataLayer,
data_layer: AshSqlite.DataLayer,
extensions: [AshHq.Docs.Extensions.Search, AshHq.Docs.Extensions.RenderMarkdown]
sqlite do
table "dsls"
repo AshHq.SqliteRepo
references do
reference :library_version, on_delete: :delete
reference :dsl, on_delete: :delete
end
migration_defaults optional_args: "[]"
end
search do
doc_attribute :doc
load_for_search [
:extension_name,
:extension_target,
:extension_module,
:library_name
]
weight_content(0.2)
sanitized_name_attribute :sanitized_path
use_path_for_name? true
end
render_markdown do
render_attributes doc: :doc_html
end
actions do
defaults [:update, :destroy]
@ -35,26 +67,6 @@ defmodule AshHq.Docs.Dsl do
end
end
search do
doc_attribute :doc
load_for_search [
:extension_name,
:extension_target,
:extension_module,
:library_name
]
weight_content(0.2)
sanitized_name_attribute :sanitized_path
use_path_for_name? true
end
render_markdown do
render_attributes doc: :doc_html
end
attributes do
uuid_primary_key :id
@ -113,18 +125,6 @@ defmodule AshHq.Docs.Dsl do
has_many :dsls, __MODULE__
end
postgres do
table "dsls"
repo AshHq.Repo
references do
reference :library_version, on_delete: :delete
reference :dsl, on_delete: :delete
end
migration_defaults optional_args: "[]"
end
code_interface do
define_for AshHq.Docs
define :read
@ -134,14 +134,14 @@ defmodule AshHq.Docs.Dsl do
description "An entity or section in an Ash DSL"
end
aggregates do
first :extension_type, :extension, :type
first :extension_order, :extension, :order
first :extension_name, :extension, :name
first :extension_module, :extension, :module
first :extension_target, :extension, :target
first :version_name, :library_version, :version
first :library_name, [:library_version, :library], :name
first :library_id, [:library_version, :library], :id
calculations do
calculate :extension_type, :string, expr(extension.type)
calculate :extension_order, :integer, expr(extension.order)
calculate :extension_name, :string, expr(extension.name)
calculate :extension_module, :string, expr(extension.module)
calculate :extension_target, :string, expr(extension.target)
calculate :version_name, :string, expr(library_version.version)
calculate :library_name, :string, expr(library_version.library.name)
calculate :library_id, :string, expr(library_version.library.id)
end
end

View file

@ -2,9 +2,27 @@ defmodule AshHq.Docs.Extension do
@moduledoc false
use Ash.Resource,
data_layer: AshPostgres.DataLayer,
data_layer: AshSqlite.DataLayer,
extensions: [AshHq.Docs.Extensions.Search, AshHq.Docs.Extensions.RenderMarkdown]
sqlite do
table "extensions"
repo AshHq.SqliteRepo
references do
reference :library_version, on_delete: :delete
end
end
search do
doc_attribute :doc
load_for_search library_version: [:library_display_name, :library_name]
end
render_markdown do
render_attributes doc: :doc_html
end
actions do
defaults [:update, :destroy]
@ -31,15 +49,6 @@ defmodule AshHq.Docs.Extension do
end
end
search do
doc_attribute :doc
load_for_search library_version: [:library_display_name, :library_name]
end
render_markdown do
render_attributes doc: :doc_html
end
attributes do
uuid_primary_key :id
@ -86,15 +95,6 @@ defmodule AshHq.Docs.Extension do
has_many :options, AshHq.Docs.Option
end
postgres do
table "extensions"
repo AshHq.Repo
references do
reference :library_version, on_delete: :delete
end
end
code_interface do
define_for AshHq.Docs

View file

@ -2,9 +2,39 @@ defmodule AshHq.Docs.Function do
@moduledoc false
use Ash.Resource,
data_layer: AshPostgres.DataLayer,
data_layer: AshSqlite.DataLayer,
extensions: [AshHq.Docs.Extensions.Search, AshHq.Docs.Extensions.RenderMarkdown]
sqlite do
table "functions"
repo AshHq.SqliteRepo
references do
reference :library_version, on_delete: :delete
end
end
search do
doc_attribute :doc
load_for_search [
:version_name,
:library_name,
:module_name,
:call_name,
:library_id
]
type "Code"
show_docs_on :module_sanitized_name
end
render_markdown do
render_attributes doc: :doc_html, heads: :heads_html
header_ids? false
end
actions do
defaults [:update, :destroy]
@ -26,26 +56,6 @@ defmodule AshHq.Docs.Function do
end
end
search do
doc_attribute :doc
load_for_search [
:version_name,
:library_name,
:module_name,
:library_id
]
type "Code"
show_docs_on :module_sanitized_name
end
render_markdown do
render_attributes doc: :doc_html, heads: :heads_html
header_ids? false
end
attributes do
uuid_primary_key :id
@ -105,15 +115,6 @@ defmodule AshHq.Docs.Function do
end
end
postgres do
table "functions"
repo AshHq.Repo
references do
reference :library_version, on_delete: :delete
end
end
code_interface do
define_for AshHq.Docs
end
@ -122,11 +123,12 @@ defmodule AshHq.Docs.Function do
description "A function in a module exposed by an Ash library"
end
aggregates do
first :version_name, :library_version, :version
first :library_name, [:library_version, :library], :name
first :library_id, [:library_version, :library], :id
first :module_name, :module, :name
first :module_sanitized_name, :module, :sanitized_name
calculations do
calculate :version_name, :string, expr(library_version.version)
calculate :library_name, :string, expr(library_version.library.name)
calculate :library_id, :uuid, expr(library_version.library.id)
calculate :module_name, :string, expr(module.name)
calculate :module_sanitized_name, :string, expr(module.sanitized_name)
calculate :call_name, :string, expr(module_name <> "." <> name)
end
end

View file

@ -1,7 +1,7 @@
defmodule AshHq.Docs.Guide do
@moduledoc false
use Ash.Resource,
data_layer: AshPostgres.DataLayer,
data_layer: AshSqlite.DataLayer,
extensions: [
AshHq.Docs.Extensions.Search,
AshHq.Docs.Extensions.RenderMarkdown,
@ -9,6 +9,43 @@ defmodule AshHq.Docs.Guide do
AshAdmin.Resource
]
sqlite do
repo AshHq.SqliteRepo
table "guides"
references do
reference :library_version, on_delete: :delete
end
end
search do
doc_attribute :text
show_docs_on [:sanitized_name, :sanitized_route]
type "Guides"
load_for_search [:library_name, library_version: [:library_name, :library_display_name]]
end
render_markdown do
render_attributes text: :text_html
table_of_contents? true
end
graphql do
type :guide
queries do
list :list_guides, :read_for_version
end
end
admin do
form do
field :text do
type :markdown
end
end
end
actions do
defaults [:create, :update, :destroy]
@ -34,34 +71,6 @@ defmodule AshHq.Docs.Guide do
end
end
search do
doc_attribute :text
show_docs_on [:sanitized_name, :sanitized_route]
type "Guides"
load_for_search library_version: [:library_name, :library_display_name]
end
render_markdown do
render_attributes text: :text_html
table_of_contents? true
end
graphql do
type :guide
queries do
list :list_guides, :read_for_version
end
end
admin do
form do
field :text do
type :markdown
end
end
end
attributes do
uuid_primary_key :id
@ -112,15 +121,6 @@ defmodule AshHq.Docs.Guide do
end
end
postgres do
repo AshHq.Repo
table "guides"
references do
reference :library_version, on_delete: :delete
end
end
code_interface do
define_for AshHq.Docs
end
@ -146,4 +146,8 @@ defmodule AshHq.Docs.Guide do
end
end
end
calculations do
calculate :library_name, :string, expr(library_version.library.name)
end
end

View file

@ -25,8 +25,6 @@ defmodule AshHq.Docs.Library.Agent do
end
def clear do
AshHq.Discord.Listener.rebuild()
Agent.update(__MODULE__, fn _state ->
nil
end)

View file

@ -1,7 +1,14 @@
defmodule AshHq.Docs.Library do
@moduledoc false
use Ash.Resource,
data_layer: AshPostgres.DataLayer
data_layer: AshSqlite.DataLayer
sqlite do
table "libraries"
repo AshHq.SqliteRepo
migration_defaults module_prefixes: "[]", skip_versions: "[]"
end
actions do
defaults [:create, :update, :destroy]
@ -67,16 +74,10 @@ defmodule AshHq.Docs.Library do
has_one :latest_library_version, AshHq.Docs.LibraryVersion do
sort version: :desc
from_many? true
end
end
postgres do
table "libraries"
repo AshHq.Repo
migration_defaults module_prefixes: "[]", skip_versions: "[]"
end
code_interface do
define_for AshHq.Docs
@ -103,13 +104,8 @@ defmodule AshHq.Docs.Library do
end
end
aggregates do
first :latest_version, :versions, :version do
sort version: :desc
end
first :latest_version_id, :versions, :id do
sort version: :desc
end
calculations do
calculate :latest_version, :string, expr(latest_library_version.version)
calculate :latest_version_id, :uuid, expr(latest_library_version.id)
end
end

View file

@ -2,9 +2,19 @@ defmodule AshHq.Docs.LibraryVersion do
@moduledoc false
use Ash.Resource,
data_layer: AshPostgres.DataLayer,
data_layer: AshSqlite.DataLayer,
extensions: [AshHq.Docs.Extensions.Search, AshHq.Docs.Extensions.RenderMarkdown]
sqlite do
table "library_versions"
repo AshHq.SqliteRepo
end
search do
name_attribute :version
load_for_search [:library_name, :library_display_name]
end
actions do
defaults [:update, :destroy]
@ -81,12 +91,6 @@ defmodule AshHq.Docs.LibraryVersion do
end
end
search do
name_attribute :version
library_version_attribute :id
load_for_search [:library_name, :library_display_name]
end
attributes do
uuid_primary_key :id
@ -113,11 +117,6 @@ defmodule AshHq.Docs.LibraryVersion do
has_many :mix_tasks, AshHq.Docs.MixTask
end
postgres do
table "library_versions"
repo AshHq.Repo
end
code_interface do
define_for AshHq.Docs
define :build, args: [:library, :version]
@ -143,18 +142,8 @@ defmodule AshHq.Docs.LibraryVersion do
end
end
preparations do
prepare AshHq.Docs.LibraryVersion.Preparations.SortBySortableVersionInstead
end
aggregates do
first :library_name, :library, :name
first :library_display_name, :library, :display_name
end
calculations do
calculate :sortable_version,
{:array, :string},
expr(fragment("string_to_array(?, '.')", version))
calculate :library_name, :string, expr(library.name)
calculate :library_display_name, :string, expr(library.display_name)
end
end

View file

@ -1,16 +0,0 @@
defmodule AshHq.Docs.LibraryVersion.Preparations.SortBySortableVersionInstead do
@moduledoc """
Replaces any sort on `version` by a sort on `sortable_version` instead.
"""
use Ash.Resource.Preparation
def prepare(query, _, _) do
%{query | sort: replace_sort(query.sort)}
end
defp replace_sort(nil), do: nil
defp replace_sort(:version), do: :sortable_version
defp replace_sort({:version, order}), do: {:sortable_version, order}
defp replace_sort(list) when is_list(list), do: Enum.map(list, &replace_sort/1)
defp replace_sort(other), do: other
end

View file

@ -2,22 +2,15 @@ defmodule AshHq.Docs.MixTask do
@moduledoc false
use Ash.Resource,
data_layer: AshPostgres.DataLayer,
data_layer: AshSqlite.DataLayer,
extensions: [AshHq.Docs.Extensions.Search, AshHq.Docs.Extensions.RenderMarkdown]
actions do
defaults [:update, :destroy]
sqlite do
table "mix_tasks"
repo AshHq.SqliteRepo
read :read do
primary? true
pagination offset?: true, countable: true, default_limit: 25, required?: false
end
create :create do
primary? true
argument :library_version, :uuid
change manage_relationship(:library_version, type: :append_and_remove)
references do
reference :library_version, on_delete: :delete
end
end
@ -39,6 +32,27 @@ defmodule AshHq.Docs.MixTask do
render_attributes doc: :doc_html
end
actions do
defaults [:update, :destroy]
read :read do
primary? true
pagination keyset?: true,
offset?: true,
countable: true,
default_limit: 25,
required?: false
end
create :create do
primary? true
argument :library_version, :uuid
change manage_relationship(:library_version, type: :append_and_remove)
end
end
attributes do
uuid_primary_key :id
@ -79,15 +93,6 @@ defmodule AshHq.Docs.MixTask do
end
end
postgres do
table "mix_tasks"
repo AshHq.Repo
references do
reference :library_version, on_delete: :delete
end
end
code_interface do
define_for AshHq.Docs
end
@ -96,9 +101,9 @@ defmodule AshHq.Docs.MixTask do
description "Represents a mix task that has been exposed by a library"
end
aggregates do
first :version_name, :library_version, :version
first :library_name, [:library_version, :library], :name
first :library_id, [:library_version, :library], :id
calculations do
calculate :version_name, :string, expr(library_version.version)
calculate :library_name, :string, expr(library_version.library.name)
calculate :library_id, :uuid, expr(library_version.library.id)
end
end

View file

@ -2,9 +2,36 @@ defmodule AshHq.Docs.Module do
@moduledoc false
use Ash.Resource,
data_layer: AshPostgres.DataLayer,
data_layer: AshSqlite.DataLayer,
extensions: [AshHq.Docs.Extensions.Search, AshHq.Docs.Extensions.RenderMarkdown]
sqlite do
table "modules"
repo AshHq.SqliteRepo
references do
reference :library_version, on_delete: :delete
end
end
search do
doc_attribute :doc
weight_content(0.5)
load_for_search [
:version_name,
:library_name,
:library_id
]
type "Code"
end
render_markdown do
render_attributes doc: :doc_html
end
actions do
defaults [:update, :destroy]
@ -29,24 +56,6 @@ defmodule AshHq.Docs.Module do
end
end
search do
doc_attribute :doc
weight_content(0.5)
load_for_search [
:version_name,
:library_name,
:library_id
]
type "Code"
end
render_markdown do
render_attributes doc: :doc_html
end
attributes do
uuid_primary_key :id
@ -87,15 +96,6 @@ defmodule AshHq.Docs.Module do
has_many :functions, AshHq.Docs.Function
end
postgres do
table "modules"
repo AshHq.Repo
references do
reference :library_version, on_delete: :delete
end
end
code_interface do
define_for AshHq.Docs
end
@ -104,9 +104,9 @@ defmodule AshHq.Docs.Module do
description "Represents a module that has been exposed by a library"
end
aggregates do
first :version_name, :library_version, :version
first :library_name, [:library_version, :library], :name
first :library_id, [:library_version, :library], :id
calculations do
calculate :version_name, :string, expr(library_version.version)
calculate :library_name, :string, expr(library_version.library.name)
calculate :library_id, :uuid, expr(library_version.library.id)
end
end

View file

@ -2,9 +2,39 @@ defmodule AshHq.Docs.Option do
@moduledoc false
use Ash.Resource,
data_layer: AshPostgres.DataLayer,
data_layer: AshSqlite.DataLayer,
extensions: [AshHq.Docs.Extensions.Search, AshHq.Docs.Extensions.RenderMarkdown]
sqlite do
table "options"
repo AshHq.SqliteRepo
references do
reference :library_version, on_delete: :delete
reference :dsl, on_delete: :delete
end
end
search do
doc_attribute :doc
load_for_search [
:extension_name,
:extension_module,
:extension_target,
:library_name
]
sanitized_name_attribute :sanitized_path
use_path_for_name? true
add_name_to_path? false
show_docs_on :dsl_sanitized_path
end
render_markdown do
render_attributes doc: :doc_html
end
actions do
defaults [:update, :destroy]
@ -31,26 +61,6 @@ defmodule AshHq.Docs.Option do
end
end
search do
doc_attribute :doc
load_for_search [
:extension_name,
:extension_module,
:extension_target,
:library_name
]
sanitized_name_attribute :sanitized_path
use_path_for_name? true
add_name_to_path? false
show_docs_on :dsl_sanitized_path
end
render_markdown do
render_attributes doc: :doc_html
end
attributes do
uuid_primary_key :id
@ -105,16 +115,6 @@ defmodule AshHq.Docs.Option do
end
end
postgres do
table "options"
repo AshHq.Repo
references do
reference :library_version, on_delete: :delete
reference :dsl, on_delete: :delete
end
end
code_interface do
define_for AshHq.Docs
define :read
@ -124,15 +124,15 @@ defmodule AshHq.Docs.Option do
description "Represents an option on a DSL section or entity"
end
aggregates do
first :extension_type, [:dsl, :extension], :type
first :extension_name, [:dsl, :extension], :name
first :extension_order, [:dsl, :extension], :order
first :extension_target, [:dsl, :extension], :target
first :extension_module, [:dsl, :extension], :module
first :version_name, :library_version, :version
first :library_name, [:library_version, :library], :name
first :library_id, [:library_version, :library], :id
first :dsl_sanitized_path, :dsl, :sanitized_path
calculations do
calculate :extension_type, :string, expr(dsl.extension.type)
calculate :extension_name, :string, expr(dsl.extension.name)
calculate :extension_order, :integer, expr(dsl.extension.order)
calculate :extension_target, :string, expr(dsl.extension.target)
calculate :extension_module, :string, expr(dsl.extension.module)
calculate :version_name, :string, expr(library_version.version)
calculate :library_name, :string, expr(library_version.library.name)
calculate :library_id, :string, expr(library_version.library.id)
calculate :dsl_sanitized_path, :string, expr(dsl.sanitized_path)
end
end

View file

@ -6,6 +6,7 @@ defmodule AshHq.Release do
@app :ash_hq
def migrate do
load_app()
System.cmd("sqlite3", ["litefs/db", "VACUUM;"])
for repo <- repos() do
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
@ -23,14 +24,7 @@ defmodule AshHq.Release do
end
defp repos do
apis()
|> Enum.flat_map(fn api ->
api
|> Ash.Api.Info.resources()
|> Enum.filter(&(AshPostgres.DataLayer in Spark.extensions(&1)))
|> Enum.map(&AshPostgres.DataLayer.Info.repo/1)
end)
|> Enum.uniq()
Application.fetch_env!(@app, :ecto_repos)
end
defp apis do

View file

@ -0,0 +1,4 @@
defmodule AshHq.SqliteRepo do
use AshSqlite.Repo,
otp_app: :ash_hq
end

View file

@ -95,6 +95,42 @@ defmodule AshHqWeb.Components.Search do
~F"""
<div class="divide-y">
{#for item <- items}
{#if item.__struct__ == AshHq.Docs.Guide}
<LivePatch
class="block w-full text-left border-base-light-300 dark:border-base-dark-600"
to={DocRoutes.doc_link(item)}
opts={id: "result-#{item.id}", "phx-click": @close}
>
<div class={
"hover:bg-base-light-100 dark:hover:bg-base-dark-750 py-1 w-full",
"bg-base-light-200 dark:bg-base-dark-700": @selected_item.id == item.id
}>
<div class="flex justify-start items-center space-x-2 pb-2 pl-2">
<div>
<Icon type={item_type(item)} classes="h-4 w-4 flex-none mt-1 mx-1" />
</div>
<div class="flex flex-col">
<div class="text-primary-light-700 dark:text-primary-dark-300">
<span class="text-primary-light-700 dark:text-primary-dark-500">
{item.library_name}
</span>
{item_type(item)}
</div>
<div class="flex flex-row flex-wrap items-center">
<div class="font-bold">
{item_name(item)}
</div>
</div>
<div>
{first_sentence(item)}
</div>
</div>
</div>
<div>
</div>
</div>
</LivePatch>
{/if}
{#if item.__struct__ != AshHq.Docs.Guide}
<a
class="block w-full text-left border-base-light-300 dark:border-base-dark-600"
@ -103,107 +139,69 @@ defmodule AshHqWeb.Components.Search do
phx-click={@close}
>
<div class={
"hover:bg-base-light-100 dark:hover:bg-base-dark-750 py-4",
"hover:bg-base-light-100 dark:hover:bg-base-dark-750 py-1 w-full",
"bg-base-light-200 dark:bg-base-dark-700": @selected_item.id == item.id
}>
<div class="flex justify-start items-center space-x-2 pb-2 pl-2">
<div>
<Icon type={item_type(item)} classes="h-4 w-4 flex-none mt-1 mx-1" />
</div>
<div class="flex flex-row flex-wrap items-center">
{#for {path_item, index} <- Enum.with_index(item_path(item))}
{#if index != 0}
<Heroicons.Solid.ChevronRightIcon class="h-4 w-4 mt-1" />
{/if}
<div>
{path_item}
<div class="flex flex-col">
<div class="text-primary-light-700 dark:text-primary-dark-300">
<span class="text-primary-light-700 dark:text-primary-dark-500">
{item.library_name}
</span>
{item_type(item)}
</div>
<div class="flex flex-row flex-wrap items-center">
<div class="font-bold">
{item_name(item)}
</div>
{/for}
<Heroicons.Solid.ChevronRightIcon class="h-4 w-4 mt-1" />
<div class="font-bold">
{item_name(item)}
</div>
<div>
{first_sentence(item)}
</div>
</div>
</div>
<div class="text-base-light-700 dark:text-base-dark-400 ml-10">
{raw(item.search_headline)}
<div>
</div>
</div>
</a>
{/if}
{#if item.__struct__ == AshHq.Docs.Guide}
<LivePatch
class="block w-full text-left border-base-light-300 dark:border-base-dark-600"
to={DocRoutes.doc_link(item)}
opts={id: "result-#{item.id}", "phx-click": @close}
>
<div class={
"hover:bg-base-light-100 dark:hover:bg-base-dark-750 py-4",
"bg-base-light-200 dark:bg-base-dark-700": @selected_item.id == item.id
}>
<div class="flex justify-start items-center space-x-2 pb-2 pl-2">
<div>
<Icon type={item_type(item)} classes="h-4 w-4 flex-none mt-1 mx-1" />
</div>
<div class="flex flex-row flex-wrap items-center">
{#for {path_item, index} <- Enum.with_index(item_path(item))}
{#if index != 0}
<Heroicons.Solid.ChevronRightIcon class="h-4 w-4 mt-1" />
{/if}
<div>
{path_item}
</div>
{/for}
<Heroicons.Solid.ChevronRightIcon class="h-4 w-4 mt-1" />
<div class="font-bold">
{item_name(item)}
</div>
</div>
</div>
<div class="text-base-light-700 dark:text-base-dark-400 ml-10">
{raw(item.search_headline)}
</div>
</div>
</LivePatch>
{/if}
{/for}
</div>
"""
end
defp item_name(%{thread_name: thread_name, channel_name: channel_name}),
do: "#{String.capitalize(channel_name)} Forum: #{inspect(thread_name)}"
defp first_sentence(%{text: text}), do: first_sentence(text)
defp first_sentence(%{doc: doc}), do: first_sentence(doc)
defp first_sentence(doc) do
first_sentence =
doc
|> String.trim()
|> String.split("<!--- heads-end -->", parts: 2)
|> List.last()
|> String.trim()
|> String.split("\n", parts: 2)
|> Enum.at(0)
|> String.trim()
if String.starts_with?(first_sentence, "`") do
""
else
first_sentence
end
end
defp item_name(%AshHq.Docs.Function{call_name: call_name, arity: arity}),
do: call_name <> "/#{arity}"
defp item_name(%AshHq.Docs.Option{path: path, name: name}), do: Enum.join(path ++ [name], ".")
defp item_name(%AshHq.Docs.Dsl{path: path, name: name}), do: Enum.join(path ++ [name], ".")
defp item_name(%{name: name}), do: name
defp item_name(%{version: version}), do: version
defp item_path(%{
library_name: library_name,
extension_name: extension_name,
path: path
}) do
[library_name, extension_name, path] |> List.flatten()
end
defp item_path(%{
library_name: library_name,
module_name: module_name
}) do
[library_name, module_name]
end
defp item_path(%{library_name: library_name}) do
[library_name]
end
defp item_path(%{library_version: %{library_name: library_name}}) do
[library_name]
end
defp item_path(_) do
[]
end
def mount(socket) do
{:ok, socket}
end
@ -273,19 +271,14 @@ defmodule AshHqWeb.Components.Search do
if socket.assigns.search in [nil, ""] do
socket
else
%{result: item_list} =
AshHq.Docs.Search.run!(
socket.assigns.search,
%{types: socket.assigns[:selected_types]}
)
item_list = Enum.take(item_list, 50)
selected_item = Enum.at(item_list, 0)
item_list =
socket.assigns.search
|> AshHq.Docs.Indexer.search()
|> Enum.take(50)
socket
|> assign(:item_list, item_list)
|> set_selected_item(selected_item)
|> set_selected_item(Enum.at(item_list, 0))
end
end

View file

@ -1,353 +0,0 @@
defmodule AshHqWeb.Pages.Ashley do
@moduledoc "Ashley page"
use Surface.LiveComponent
import AshHqWeb.Tails
alias Phoenix.LiveView.JS
alias Surface.Components.Form
alias Surface.Components.Form.{
Field,
TextArea,
TextInput
}
prop(current_user, :any)
prop(params, :map)
data(messages, :list)
data(message_form, :any)
data(new_message_form, :any)
data(conversation, :any)
data(conversations, :list)
data(editing_conversation, :boolean)
data(conversation_form, :any)
def render(assigns) do
~F"""
<div class="lg:grid lg:grid-cols-6 lg:w-2/3 lg:mx-auto mx-8">
{#if is_nil(@current_user) || !@current_user.ashley_access}
You do not have access to this page.
{#else}
<div class="grid-cols-1 flex-col w-full hidden lg:block mr-2">
<a
href="/ashley"
class="p-2 rounded-md bg-gray-300 dark:bg-gray-800 w-full flex flex-row items-center hover:bg-gray-500 hover:dark:bg-gray-600"
>
<Heroicons.Solid.PlusIcon class="h-4 w-4" /> New
</a>
{#for conversation <- @conversations}
<div class={classes([
"p-2",
["text-gray-500": conversation.question_count >= AshHq.Ashley.Conversation.conversation_limit()]
])}>
<a href={"/ashley/#{conversation.id}"}>
{conversation.name} - {conversation.question_count}</a>
</div>
{/for}
</div>
<div class="grid-cols-1 flex-col w-full block lg:hidden">
<div class="flex flex-row items-center justify-end mb-4">
<a
href="/ashley"
class="p-2 rounded-md bg-gray-300 dark:bg-gray-800 flex flex-row items-center w-24 h-12"
>
<Heroicons.Solid.PlusIcon class="h-4 w-4" /> New
</a>
<button
phx-click={JS.toggle(to: "#mobile-conversations")}
class="p-2 rounded-md bg-gray-300 dark:bg-gray-800 w-12 h-12 ml-4 flex flex-row justify-center items-center"
>
<Heroicons.Outline.MenuIcon class="h-4 w-4" />
</button>
</div>
<div class="flex flex-col" id="mobile-conversations" style="display: none;">
{#for conversation <- @conversations}
<div class={classes([
"p-2",
["text-gray-500": conversation.question_count >= AshHq.Ashley.Conversation.conversation_limit()]
])}>
<a href={"/ashley/#{conversation.id}"}>
{conversation.name} - {conversation.question_count}</a>
</div>
{/for}
</div>
</div>
<div
id="chat-window"
class="col-span-5 flex-col prose prose-xl dark:prose-invert h-screen w-full"
>
{#if @conversation}
{#if @editing_conversation}
<Form for={@conversation_form} submit="save_conversation" class="flex flex-row justify-between">
<Field name={:name} class="w-full">
<TextInput class="flex-grow text-black block focus:ring-primary-light-600 focus:primary-light-primary-light-600 min-w-0 rounded-md sm:text-sm border-base-light-300 w-full" />
</Field>
<button type="submit">
<Heroicons.Outline.SaveIcon class="h-5 w-5" />
</button>
</Form>
{#else}
<div class="flex flex-row w-full justify-between">
<div>
{@conversation.name}
</div>
<div class="flex flex-row">
<button :on-click="toggle-editing-conversation" class="mr-4">
<Heroicons.Outline.PencilIcon class="h-5 w-5" />
</button>
<button
:on-click="destroy-conversation"
phx-value-conversation-id={@conversation.id}
data-confirm="Are you sure?"
>
<Heroicons.Outline.XIcon class="h-5 w-5" />
</button>
</div>
</div>
{/if}
<div class="overflow-y-scroll h-2/3">
<div style="scroll-snap-type: y; scroll-snap-align: end;">
<div>
<div>
Hello! My name is Ashley. I've been instructed to answer your questions as factually as possible, but I am *far* from perfect.
My code snippets especially are not likely to be accurate. However, I cite my sources below each answer to show you what content
I thought was relevant, so please use that for official clarification.
</div>
</div>
<hr>
{#for question <- @conversation.questions}
<div>
<div class="font-light mt-12">
{question.question}
</div>
<hr>
<div>
{raw(question.answer_html)}
</div>
{#if question.sources != []}
<h3>Sources</h3>
<ul>
{#for source <- question.sources}
<li><a href={"https://ash-hq.org/#{source.link}"}>{source.name}</a></li>
{/for}
</ul>
{/if}
</div>
{/for}
</div>
</div>
<div class="mt-12 w-full">
<Form for={@message_form} submit="save_message">
<div class="flex flex-row w-full">
<div class="w-full">
<span class="text-sm font-extralight">{@conversation.question_count} of {AshHq.Ashley.Conversation.conversation_limit()} used</span>
<Field name={:question} class="w-full">
<TextArea
class="flex-grow text-black block focus:ring-primary-light-600 focus:primary-light-primary-light-600 min-w-0 rounded-md sm:text-sm border-base-light-300 w-full"
opts={disabled: @conversation.question_count >= AshHq.Ashley.Conversation.conversation_limit()}
/>
</Field>
</div>
<button
type="submit"
class="p-4 rounded-xl bg-gray-200 dark:bg-gray-600 flex flex-row items-center ml-12 h-12"
>
<Heroicons.Outline.PaperAirplaneIcon class="h-4 w-4" /><div>Submit</div>
</button>
</div>
</Form>
</div>
{#else}
<Form for={@new_message_form} submit="save_new_message">
<div class="flex flex-row w-full">
<div class="w-full">
<Field name={:conversation_name} class="mb-4">
<TextInput
class="flex-grow text-black block focus:ring-primary-light-600 focus:primary-light-primary-light-600 min-w-0 rounded-md sm:text-sm border-base-light-300 w-full"
opts={placeholder: "New Conversation"}
/>
</Field>
<div>
<div>
Hello! My name is Ashley. I've been instructed to answer your questions as factually as possible, but I am *far* from perfect.
My code snippets especially are not likely to be accurate. However, I cite my sources below each answer to show you what content
I thought was relevant, so please use that for official clarification.
</div>
</div>
<hr>
<div class="flex flex-col space-y-4 lg:flex-row lg:justify-between items-center justify-center">
<Field name={:question} class="w-full">
<TextArea class="flex-grow text-black block focus:ring-primary-light-600 focus:primary-light-primary-light-600 min-w-0 rounded-md sm:text-sm border-base-light-300 w-full" />
</Field>
<button
type="submit_new_message"
class="p-4 rounded-xl bg-gray-200 dark:bg-gray-600 flex flex-row items-center lg:ml-12 h-12"
>
<Heroicons.Outline.PaperAirplaneIcon class="h-4 w-4" /><div>Submit</div>
</button>
</div>
</div>
</div>
</Form>
{/if}
</div>
{/if}
</div>
"""
end
def update(assigns, socket) do
{:ok,
socket
|> assign(assigns)
|> assign(editing_conversation: false)
|> assign_conversations()
|> assign_conversation()
|> assign_message_form()
|> assign_change_name_form()
|> assign_new_message_form}
end
defp assign_change_name_form(socket) do
if socket.assigns[:conversation] do
assign(
socket,
:conversation_form,
AshPhoenix.Form.for_update(socket.assigns.conversation, :update,
actor: socket.assigns.current_user,
api: AshHq.Ashley,
as: "conversation"
)
)
else
assign(socket, conversation_form: nil)
end
end
defp assign_conversations(socket) do
if socket.assigns[:current_user] do
assign(socket,
conversations:
AshHq.Ashley.Conversation.read!(
actor: socket.assigns.current_user,
load: :question_count
)
)
else
assign(socket, conversations: [])
end
end
defp assign_conversation(socket) do
if socket.assigns[:current_user] && socket.assigns[:params]["conversation_id"] do
assign(socket,
conversation:
Enum.find(
socket.assigns.conversations,
&(&1.id == socket.assigns[:params]["conversation_id"])
)
|> AshHq.Ashley.load!(:questions, actor: socket.assigns.current_user)
)
else
assign(socket, :conversation, nil)
end
end
defp assign_message_form(socket) do
if socket.assigns[:current_user] && socket.assigns[:conversation] do
assign(
socket,
:message_form,
AshPhoenix.Form.for_create(AshHq.Ashley.Question, :ask,
actor: socket.assigns[:current_user],
api: AshHq.Ashley,
as: "message",
prepare_source: fn changeset ->
Ash.Changeset.force_change_attribute(
changeset,
:conversation_id,
socket.assigns.conversation.id
)
end
)
)
else
assign(socket, :message_form, nil)
end
end
defp assign_new_message_form(socket) do
if socket.assigns[:current_user] do
assign(
socket,
:new_message_form,
AshPhoenix.Form.for_create(AshHq.Ashley.Question, :ask,
actor: socket.assigns[:current_user],
api: AshHq.Ashley,
as: "new_message"
)
)
else
assign(socket, :new_message_form, nil)
end
end
def handle_event("save_conversation", %{"conversation" => conversation_params}, socket) do
case AshPhoenix.Form.submit(socket.assigns.conversation_form, params: conversation_params) do
{:ok, _message} ->
{:noreply,
socket
|> assign_conversations()
|> assign_conversation()
|> assign(editing_conversation: false)
|> assign_message_form()}
{:error, form} ->
{:noreply, assign(socket, conversation_form: form)}
end
end
def handle_event("save_message", %{"message" => message_params}, socket) do
case AshPhoenix.Form.submit(socket.assigns.message_form, params: message_params) do
{:ok, _message} ->
{:noreply,
socket
|> assign_conversations()
|> assign_conversation()
|> assign_message_form()}
{:error, form} ->
{:noreply, assign(socket, message_form: form)}
end
end
def handle_event("save_new_message", %{"new_message" => message_params}, socket) do
case AshPhoenix.Form.submit(socket.assigns.new_message_form, params: message_params) do
{:ok, message} ->
{:noreply,
socket
|> push_patch(to: "/ashley/#{message.conversation_id}")}
{:error, form} ->
{:noreply, assign(socket, message_form: form)}
end
end
def handle_event("toggle-editing-conversation", _, socket) do
{:noreply, assign(socket, editing_conversation: true)}
end
def handle_event("destroy-conversation", %{"conversation-id" => conversation_id}, socket) do
case Enum.split_with(socket.assigns.conversations, &(&1.id == conversation_id)) do
{[conversation], rest} ->
AshHq.Ashley.Conversation.destroy!(conversation, actor: socket.assigns.current_user)
{:noreply, assign(socket, conversations: rest, conversation: nil)}
_ ->
{:noreply, socket}
end
end
end

View file

@ -6,7 +6,7 @@ defmodule AshHqWeb.AppViewLive do
alias AshHqWeb.Components.AppView.TopBar
alias AshHqWeb.Components.Search
alias AshHqWeb.Pages.{Ashley, Blog, Community, Docs, Home, Media, UserSettings}
alias AshHqWeb.Pages.{Blog, Community, Docs, Home, Media, UserSettings}
alias Phoenix.LiveView.JS
alias Surface.Components.Context
@ -111,11 +111,9 @@ defmodule AshHqWeb.AppViewLive do
<UserSettings id="user_settings" current_user={@current_user} />
{#match :media}
<Media id="media" />
{#match :ashley}
<Ashley id="ashley" current_user={@current_user} params={@params} />
{/case}
{#if @live_action not in [:docs_dsl, :ashley]}
{#if @live_action != :docs_dsl}
<footer class="p-8 sm:p-6 bg-base-light-200 dark:bg-base-dark-850 sm:justify-center sticky">
<div class="md:flex md:justify-around">
<div class="flex justify-center flex-row mb-6 md:mb-0">

53
litefs.yml Normal file
View file

@ -0,0 +1,53 @@
# The fuse section describes settings for the FUSE file system. This file system
# is used as a thin layer between the SQLite client in your application and the
# storage on disk. It intercepts disk writes to determine transaction boundaries
# so that those transactions can be saved and shipped to replicas.
fuse:
dir: "/litefs"
# The data section describes settings for the internal LiteFS storage. We'll
# mount a volume to the data directory so it can be persisted across restarts.
# However, this data should not be accessed directly by the user application.
data:
dir: "/var/lib/litefs"
# This flag ensure that LiteFS continues to run if there is an issue on starup.
# It makes it easy to ssh in and debug any issues you might be having rather
# than continually restarting on initialization failure.
exit-on-error: false
# This section defines settings for the option HTTP proxy.
# This proxy can handle primary forwarding & replica consistency
# for applications that use a single SQLite database.
proxy:
addr: ":8080"
target: "localhost:4000"
db: "db"
passthrough:
- "*.ico"
- "*.png"
# This section defines a list of commands to run after LiteFS has connected
# and sync'd with the cluster. You can run multiple commands but LiteFS expects
# the last command to be long-running (e.g. an application server). When the
# last command exits, LiteFS is shut down.
exec:
# Only run migrations on candidate nodes.
- cmd: "./_build/prod/rel/ash_hq/bin/ash_hq eval 'AshHq.Release.migrate'"
if-candidate: true
- cmd: "./_build/prod/rel/ash_hq/bin/ash_hq start"
# The lease section specifies how the cluster will be managed. We're using the
# "consul" lease type so that our application can dynamically change the primary.
#
# These environment variables will be available in your Fly.io application.
lease:
type: "consul"
advertise-url: "http://${HOSTNAME}.vm.${FLY_APP_NAME}.internal:20202"
candidate: ${FLY_REGION == PRIMARY_REGION}
promote: true
consul:
url: "${FLY_CONSUL_URL}"
key: "litefs/${FLY_APP_NAME}"

View file

@ -41,6 +41,7 @@ defmodule AshHq.MixProject do
[
{:ash, github: "ash-project/ash", override: true},
{:ash_postgres, github: "ash-project/ash_postgres"},
{:ash_sqlite, github: "ash-project/ash_sqlite"},
{:ash_admin, github: "ash-project/ash_admin"},
{:ash_phoenix, github: "ash-project/ash_phoenix", override: true},
{:ash_graphql, github: "ash-project/ash_graphql"},
@ -50,6 +51,7 @@ defmodule AshHq.MixProject do
{:ash_authentication_phoenix, "~> 1.6"},
{:ash_blog, github: "ash-project/ash_blog"},
{:ash_csv, github: "ash-project/ash_csv"},
{:spark, github: "ash-project/spark", override: true},
# HTTP calls
{:req, "~> 0.4.3"},
# Appsignal
@ -59,11 +61,8 @@ defmodule AshHq.MixProject do
# Discord
{:nostrum, github: "zachdaniel/nostrum"},
{:cowlib, "~> 2.11", hex: :remedy_cowlib, override: true},
# Ashley
{:open_ai, github: "hernanat/open_ai_ex"},
{:pinecone, "~> 0.1.0"},
# Tiktoken isn't working for some reason
# {:tiktoken, "~> 0.1.0"}
# Search
{:haystack, "~> 0.1.0"},
# Clustering
{:libcluster, "~> 3.3"},
# UI

View file

@ -4,7 +4,7 @@
"appsignal": {:hex, :appsignal, "2.7.9", "efc11601a848f153752778356bc86f9af03e925f15a961f714cd702a83cee434", [:make, :mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:decorator, "~> 1.2.3 or ~> 1.3", [hex: :decorator, repo: "hexpm", optional: false]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, ">= 1.3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "84fff1dcba3d3fcf542c528ffb0f4eba891da0f68125fcf57e9b30d1a1a4a6e9"},
"appsignal_phoenix": {:hex, :appsignal_phoenix, "2.3.4", "c83a8e15a51456db7d722a21bfe9a45e23618b550219caa8fb6d4853f61b5734", [:mix], [{:appsignal, ">= 2.7.6 and < 3.0.0", [hex: :appsignal, repo: "hexpm", optional: false]}, {:appsignal_plug, ">= 2.0.15 and < 3.0.0", [hex: :appsignal_plug, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.11 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 0.9", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1ca040fbfa653bdda25735031d8b89c81ebb50c475bc35d5ac0e13236e5f6600"},
"appsignal_plug": {:hex, :appsignal_plug, "2.0.15", "758a8a78944878e8461bbc77ca86219121a56f4299c6d79940ab083cf9afea00", [:mix], [{:appsignal, ">= 2.7.6 and < 3.0.0", [hex: :appsignal, repo: "hexpm", optional: false]}, {:plug, ">= 1.1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1c6059049e2081e808aaef04e2b9917e06277f61a35a0e103db860d08cbc41f1"},
"ash": {:git, "https://github.com/ash-project/ash.git", "74fbd8a6fe85be3f4b61682a824f0e4d2dc9cabd", []},
"ash": {:git, "https://github.com/ash-project/ash.git", "b93cbb95775d3e5f9166167134cafea26a94d2fc", []},
"ash_admin": {:git, "https://github.com/ash-project/ash_admin.git", "3002af9ec69dc475582ef5f445064e4594bf45ac", []},
"ash_appsignal": {:hex, :ash_appsignal, "0.1.2", "a6eb1927a13c11006aad0d9ffaa011143344dd04c9b07ab94f459498b8ddc6d4", [:mix], [{:appsignal, "~> 2.0", [hex: :appsignal, repo: "hexpm", optional: false]}, {:ash, ">= 2.14.14", [hex: :ash, repo: "hexpm", optional: false]}], "hexpm", "dae3158337d2a36b76f04519ebe6d08ef5296823831993cef6069eeb879c5b94"},
"ash_authentication": {:hex, :ash_authentication, "3.11.15", "7834446cdd13bb471bded630aa0e0e4fb8795ffffe0c294dc22448d3778ff035", [:mix], [{:ash, ">= 2.5.11 and < 3.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:assent, "~> 0.2", [hex: :assent, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:finch, "~> 0.16.0", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:joken, "~> 2.5", [hex: :joken, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}, {:spark, ">= 1.1.39 and < 2.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "931926d8dd3fc5ac54a354d69aad6aed30438977d7971bf3fdfae4c563557d21"},
@ -14,12 +14,13 @@
"ash_graphql": {:git, "https://github.com/ash-project/ash_graphql.git", "cf57747d79f848608252ab167d74bb59aaef2345", []},
"ash_json_api": {:git, "https://github.com/ash-project/ash_json_api.git", "31ece4fad9920c7e45c600d38ac82218296c4612", []},
"ash_phoenix": {:git, "https://github.com/ash-project/ash_phoenix.git", "35e4d2931e1664383c9a085a90f846e58986c8c8", []},
"ash_postgres": {:git, "https://github.com/ash-project/ash_postgres.git", "3e2826db04ad537137f99a69bcf275bc16f75b18", []},
"ash_postgres": {:git, "https://github.com/ash-project/ash_postgres.git", "41c34b8777777a116ea87fe93d3b5603ef9ac030", []},
"ash_sqlite": {:git, "https://github.com/ash-project/ash_sqlite.git", "b4ed3806c4ef51c06c682956f24fb2895a0a7e54", []},
"assent": {:hex, :assent, "0.2.7", "aa68f68e577077c091ce722bff8fe1ae56b95b274bb8107f7a5406cc15a65da7", [:mix], [{:certifi, ">= 0.0.0", [hex: :certifi, repo: "hexpm", optional: true]}, {:finch, "~> 0.15", [hex: :finch, repo: "hexpm", optional: true]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: true]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:ssl_verify_fun, ">= 0.0.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: true]}], "hexpm", "08106af439de4f9de114c0334de4c848de7cfbe53a5a52d342a784c4f6bc86f3"},
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.1.0", "0b110a9a6c619b19a7f73fa3004aa11d6e719a67e672d1633dc36b6b2290a0f7", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2ad2acb5a8bc049e8d5aa267802631912bb80d5f4110a178ae7999e69dca1bf7"},
"bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},
"bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"},
"castore": {:hex, :castore, "1.0.3", "7130ba6d24c8424014194676d608cb989f62ef8039efd50ff4b3f33286d06db8", [:mix], [], "hexpm", "680ab01ef5d15b161ed6a95449fac5c6b8f60055677a8e79acf01b27baa4390b"},
"cc_precompiler": {:hex, :cc_precompiler, "0.1.8", "933a5f4da3b19ee56539a076076ce4d7716d64efc8db46fd066996a7e46e2bfd", [:mix], [{:elixir_make, "~> 0.7.3", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "176bdf4366956e456bf761b54ad70bc4103d0269ca9558fd7cee93d1b3f116db"},
"certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"},
"chacha20": {:hex, :chacha20, "1.0.4", "0359d8f9a32269271044c1b471d5cf69660c362a7c61a98f73a05ef0b5d9eb9e", [:mix], [], "hexpm", "2027f5d321ae9903f1f0da7f51b0635ad6b8819bc7fe397837930a2011bc2349"},
"cloak": {:hex, :cloak, "1.1.2", "7e0006c2b0b98d976d4f559080fabefd81f0e0a50a3c4b621f85ceeb563e80bb", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "940d5ac4fcd51b252930fd112e319ea5ae6ab540b722f3ca60a85666759b9585"},
@ -43,6 +44,7 @@
"ecto": {:hex, :ecto, "3.10.3", "eb2ae2eecd210b4eb8bece1217b297ad4ff824b4384c0e3fdd28aaf96edd6135", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "44bec74e2364d491d70f7e42cd0d690922659d329f6465e89feb8a34e8cd3433"},
"ecto_psql_extras": {:hex, :ecto_psql_extras, "0.7.10", "e14d400930f401ca9f541b3349212634e44027d7f919bbb71224d7ac0d0e8acd", [:mix], [{:ecto_sql, "~> 3.4", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.15.7 or ~> 0.16.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "505e8cd81e4f17c090be0f99e92b1b3f0fd915f98e76965130b8ccfb891e7088"},
"ecto_sql": {:hex, :ecto_sql, "3.10.2", "6b98b46534b5c2f8b8b5f03f126e75e2a73c64f3c071149d32987a5378b0fdbd", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "68c018debca57cb9235e3889affdaec7a10616a4e3a80c99fa1d01fdafaa9007"},
"ecto_sqlite3": {:hex, :ecto_sqlite3, "0.12.0", "9ee845ac45a76e3c5c0fe65898f3538f5b0969912a95f0beef3d4ae8e63f6a06", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.9", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "4eaf8550df1fd0043bcf039a5dce407fd8afc30a115ced173fe6b9815eeedb55"},
"ed25519": {:hex, :ed25519, "1.4.1", "479fb83c3e31987c9cad780e6aeb8f2015fb5a482618cdf2a825c9aff809afc4", [:mix], [], "hexpm", "0dacb84f3faa3d8148e81019ca35f9d8dcee13232c32c9db5c2fb8ff48c80ec7"},
"eflame": {:hex, :eflame, "1.0.1", "0664d287e39eef3c413749254b3af5f4f8b00be71c1af67d325331c4890be0fc", [:mix], [], "hexpm", "e0b08854a66f9013129de0b008488f3411ae9b69b902187837f994d7a99cf04e"},
"elixir_make": {:hex, :elixir_make, "0.7.7", "7128c60c2476019ed978210c245badf08b03dbec4f24d05790ef791da11aa17c", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "5bc19fff950fad52bbe5f211b12db9ec82c6b34a9647da0c2224b8b8464c7e6c"},
@ -53,6 +55,7 @@
"ex_check": {:hex, :ex_check, "0.14.0", "d6fbe0bcc51cf38fea276f5bc2af0c9ae0a2bb059f602f8de88709421dae4f0e", [:mix], [], "hexpm", "8a602e98c66e6a4be3a639321f1f545292042f290f91fa942a285888c6868af0"},
"ex_doc": {:hex, :ex_doc, "0.28.5", "3e52a6d2130ce74d096859e477b97080c156d0926701c13870a4e1f752363279", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "d2c4b07133113e9aa3e9ba27efb9088ba900e9e51caa383919676afdf09ab181"},
"excoveralls": {:hex, :excoveralls, "0.14.6", "610e921e25b180a8538229ef547957f7e04bd3d3e9a55c7c5b7d24354abbba70", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "0eceddaa9785cfcefbf3cd37812705f9d8ad34a758e513bb975b081dce4eb11e"},
"exqlite": {:hex, :exqlite, "0.15.0", "efa87268ffb648a29d887d6b5641cea6d0c7cdc09b83e92a34377ab5b96913c1", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "bf704989342b99ea5e1621495fb94fc772516206550d3b2465a1de3d89345853"},
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
"finch": {:hex, :finch, "0.16.0", "40733f02c89f94a112518071c0a91fe86069560f5dbdb39f9150042f44dcfb1a", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f660174c4d519e5fec629016054d60edd822cdfe2b7270836739ac2f97735ec5"},
"floki": {:hex, :floki, "0.33.1", "f20f1eb471e726342b45ccb68edb9486729e7df94da403936ea94a794f072781", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "461035fd125f13fdf30f243c85a0b1e50afbec876cbf1ceefe6fddd2e6d712c6"},
@ -62,8 +65,10 @@
"git_ops": {:hex, :git_ops, "2.5.1", "94ab6e3bc69fe765a62cbdb09969016613a154dec8fc4f6ebae682f030451da9", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "1219edc8810dcea40472ec5b7ed04786a9e1b0e4e49d8642b0e1cdfb8a6ad261"},
"gun": {:hex, :remedy_gun, "2.0.1", "0f0caed812ed9e4da4f144df2d5bf73b0a99481d395ecde990a3791decf321c6", [:rebar3], [{:cowlib, "~> 2.11.1", [hex: :remedy_cowlib, repo: "hexpm", optional: false]}], "hexpm", "b6685a85fbd12b757f86809be1b3d88fcef365b77605cd5aa34db003294c446e"},
"hackney": {:hex, :hackney, "1.19.1", "59de4716e985dd2b5cbd4954fa1ae187e2b610a9c4520ffcb0b1653c3d6e5559", [:rebar3], [{:certifi, "~> 2.12.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.1", [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", "8aa08234bdefc269995c63c2282cf3cd0e36febe3a6bfab11b610572fdd1cad0"},
"haystack": {:hex, :haystack, "0.1.0", "6cb9c72caf40ed4a5f9a094e6b09993c68c3fda0e01280c60c331a19860c504c", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:stemmer, "~> 1.1", [hex: :stemmer, repo: "hexpm", optional: false]}], "hexpm", "27a582513ef933c1b11345b96f8d41ee137d03b25312bd85068ffe8fec503635"},
"hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
"html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"},
"httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
"joken": {:hex, :joken, "2.6.0", "b9dd9b6d52e3e6fcb6c65e151ad38bf4bc286382b5b6f97079c47ade6b1bcc6a", [:mix], [{:jose, "~> 1.11.5", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5a95b05a71cd0b54abd35378aeb1d487a23a52c324fa7efdffc512b655b5aaa7"},
@ -79,16 +84,15 @@
"makeup_html": {:hex, :makeup_html, "0.1.0", "b0228fda985e311d8f0d25bed58f8280826633a38d7448cabdd723e116165bcf", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "0ca44e7dcb8d933e010740324470dd8ec947243b51304bd34b8165ef3281edc2"},
"makeup_js": {:hex, :makeup_js, "0.1.0", "ffa8ce9db95d14dcd09045334539d5992d540d63598c592d4805b7674bdd6675", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "3f0c1a5eb52c9737b1679c926574e83bb260ccdedf08b58ee96cca7c685dea75"},
"makeup_sql": {:hex, :makeup_sql, "0.1.0", "197a8a0a38e83885f73767530739bb8f990aecf7fd1597d3141608c14f5f233e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "556e23ff88ad2fb8c44e393467cfba0c4f980cbe90316deaf48a1362f58cd118"},
"meilisearch": {:hex, :meilisearch, "0.20.0", "3606a7dc9c51b8e7d890761a9614dd56812da8bfcbf4e86934ae42233b257b18", [:mix], [{:httpoison, "~> 1.8", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "50faca2a72128bc93e6771bc66dadc347357c0a2d619e743e620ce131c7798fd"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"mint": {:hex, :mint, "1.5.1", "8db5239e56738552d85af398798c80648db0e90f343c8469f6c6d8898944fb6f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "4a63e1e76a7c3956abd2c72f370a0d0aecddc3976dea5c27eccbecfa5e7d5b1e"},
"multipart": {:hex, :multipart, "0.3.1", "886d77125f5d7ba6be2f86e4be8f6d3556684c8e56a777753f06234885b09cde", [:mix], [{:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm", "9657783995d2b9b546d9c66e1d497fcb473d813a8a3fb73faf5e411538b1db97"},
"nimble_options": {:hex, :nimble_options, "0.5.2", "42703307b924880f8c08d97719da7472673391905f528259915782bb346e0a1b", [:mix], [], "hexpm", "4da7f904b915fd71db549bcdc25f8d56f378ef7ae07dc1d372cbe72ba950dce0"},
"nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"},
"nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"},
"nostrum": {:git, "https://github.com/zachdaniel/nostrum.git", "00110f97fb2a9534cf506bca01b79fd092b32581", []},
"open_ai": {:git, "https://github.com/hernanat/open_ai_ex.git", "e95b6a7b748947a0783c6cb38edbbfc209f1f42d", []},
"open_api_spex": {:hex, :open_api_spex, "3.16.3", "11bc9798890073e516a97392d5846a235925e48ecbb468cb5b1cc207d5785a3e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "1bcbe6efab88f5d001c2fc377e0bd6058180aa31b68d32962d4926e934b8ecad"},
"parallel_stream": {:hex, :parallel_stream, "1.1.0", "f52f73eb344bc22de335992377413138405796e0d0ad99d995d9977ac29f1ca9", [:mix], [], "hexpm", "684fd19191aedfaf387bbabbeb8ff3c752f0220c8112eb907d797f4592d6e871"},
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
@ -102,7 +106,6 @@
"phoenix_template": {:hex, :phoenix_template, "1.0.3", "32de561eefcefa951aead30a1f94f1b5f0379bc9e340bb5c667f65f1edfa4326", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "16f4b6588a4152f3cc057b9d0c0ba7e82ee23afa65543da535313ad8d25d8e2c"},
"phoenix_view": {:hex, :phoenix_view, "2.0.2", "6bd4d2fd595ef80d33b439ede6a19326b78f0f1d8d62b9a318e3d9c1af351098", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "a929e7230ea5c7ee0e149ffcf44ce7cf7f4b6d2bfe1752dd7c084cdff152d36f"},
"picosat_elixir": {:hex, :picosat_elixir, "0.2.3", "bf326d0f179fbb3b706bb2c15fbc367dacfa2517157d090fdfc32edae004c597", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f76c9db2dec9d2561ffaa9be35f65403d53e984e8cd99c832383b7ab78c16c66"},
"pinecone": {:hex, :pinecone, "0.1.0", "65c14c7f178fa97350db65685247ba5de641f9379d1e0b78c77183dc2fe7d199", [:mix], [{:bypass, "~> 2.1", [hex: :bypass, repo: "hexpm", optional: false]}, {:tesla, "~> 1.4", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "0212d5f0134cb724d5b6cfd224bcd739fd075c0b6271d0bfc0f4fc0f31b48f13"},
"plug": {:hex, :plug, "1.14.2", "cff7d4ec45b4ae176a227acd94a7ab536d9b37b942c8e8fa6dfc0fff98ff4d80", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "842fc50187e13cf4ac3b253d47d9474ed6c296a8732752835ce4a86acdf68d13"},
"plug_content_security_policy": {:hex, :plug_content_security_policy, "0.2.1", "0a19c76307ad000b3757739c14b34b83ecccf7d0a3472e64e14797a20b62939b", [:mix], [{:plug, "~> 1.3", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ceea10050671c0387c64526e2cb337ee08e12705c737eaed80439266df5b2e29"},
"plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"},
@ -116,8 +119,9 @@
"slugify": {:hex, :slugify, "1.3.1", "0d3b8b7e5c1eeaa960e44dce94382bee34a39b3ea239293e457a9c5b47cc6fd3", [:mix], [], "hexpm", "cb090bbeb056b312da3125e681d98933a360a70d327820e4b7f91645c4d8be76"},
"sobelow": {:hex, :sobelow, "0.11.1", "23438964486f8112b41e743bbfd402da3e5b296fdc9eacab29914b79c48916dd", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "9897363a7eff96f4809304a90aad819e2ad5e5d24db547af502885146746a53c"},
"sourceror": {:hex, :sourceror, "0.14.0", "b6b8552d0240400d66b6f107c1bab7ac1726e998efc797f178b7b517e928e314", [:mix], [], "hexpm", "809c71270ad48092d40bbe251a133e49ae229433ce103f762a2373b7a10a8d8b"},
"spark": {:hex, :spark, "1.1.45", "85d02ed5623079a304dbff2ff63bc5cac37301005f7cb72e727f1d1dbdcd5db3", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.5 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:sourceror, "~> 0.1", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "538d7ff671f19a086458147eb04c38251175c60afed204231fb4848e8cdfb378"},
"spark": {:git, "https://github.com/ash-project/spark.git", "92ba4bd227a2f0d4f55d5a2438e8af6039913fbc", []},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
"stemmer": {:hex, :stemmer, "1.1.0", "71221331ced40832b47e6989a12dd9de1b15c982043d1014742be83c34ec9e79", [:mix], [], "hexpm", "0cb5faf73476b84500e371ff39fd9a494f60ab31d991689c1cd53b920556228f"},
"stream_data": {:hex, :stream_data, "0.6.0", "e87a9a79d7ec23d10ff83eb025141ef4915eeb09d4491f79e52f2562b73e5f47", [:mix], [], "hexpm", "b92b5031b650ca480ced047578f1d57ea6dd563f5b57464ad274718c9c29501c"},
"sunflower_ui": {:git, "https://github.com/zachdaniel/sunflower_ui.git", "3ec87f33e003693e6db2329f9d6d8ac59983cf17", []},
"surface": {:hex, :surface, "0.9.4", "60307da284715de50a21ff0f78a27d4c7db48e88afddd3794319bfc534c0051e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18.14", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:sourceror, "~> 0.11", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "a29312c6346f0662dbf8d1d364a0fbf9f53fc469c375bf2b1efcbd0f1bc9844c"},
@ -128,7 +132,6 @@
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
"telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"},
"tesla": {:hex, :tesla, "1.7.0", "a62dda2f80d4f8a925eb7b8c5b78c461e0eb996672719fe1a63b26321a5f8b4e", [: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.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [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]}, {: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", "2e64f01ebfdb026209b47bc651a0e65203fcff4ae79c11efb73c4852b00dc313"},
"typable": {:hex, :typable, "0.3.0", "0431e121d124cd26f312123e313d2689b9a5322b15add65d424c07779eaa3ca1", [:mix], [], "hexpm", "880a0797752da1a4c508ac48f94711e04c86156f498065a83d160eef945858f8"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},

View file

@ -0,0 +1,32 @@
defmodule AshHq.Repo.Migrations.MigrateResources58 do
@moduledoc """
Updates resources based on their most recent snapshots.
This file was autogenerated with `mix ash_postgres.generate_migrations`
"""
use Ecto.Migration
def up do
execute("""
ALTER TABLE discord_messages
DROP COLUMN searchable
""")
drop_if_exists(
index(:discord_messages, ["searchable"], name: "discord_messages_searchable_index")
)
end
def down do
create index(:discord_messages, ["searchable"], using: "GIN")
execute("""
ALTER TABLE discord_messages
ADD COLUMN searchable tsvector
GENERATED ALWAYS AS (
setweight(to_tsvector('english', content), 'D')
) STORED;
""")
end
end

View file

@ -0,0 +1,95 @@
{
"attributes": [
{
"default": "nil",
"size": null,
"type": "bigint",
"source": "id",
"references": null,
"allow_nil?": false,
"primary_key?": true,
"generated?": false
},
{
"default": "nil",
"size": null,
"type": "text",
"source": "author",
"references": null,
"allow_nil?": false,
"primary_key?": false,
"generated?": false
},
{
"default": "nil",
"size": null,
"type": "text",
"source": "content",
"references": null,
"allow_nil?": true,
"primary_key?": false,
"generated?": false
},
{
"default": "nil",
"size": null,
"type": "text",
"source": "content_html",
"references": null,
"allow_nil?": true,
"primary_key?": false,
"generated?": false
},
{
"default": "nil",
"size": null,
"type": "utc_datetime",
"source": "timestamp",
"references": null,
"allow_nil?": false,
"primary_key?": false,
"generated?": false
},
{
"default": "nil",
"size": null,
"type": "bigint",
"source": "thread_id",
"references": {
"name": "discord_messages_thread_id_fkey",
"table": "discord_threads",
"schema": "public",
"destination_attribute": "id",
"on_delete": "delete",
"on_update": "update",
"deferrable": false,
"primary_key?": true,
"multitenancy": {
"global": null,
"strategy": null,
"attribute": null
},
"destination_attribute_default": null,
"destination_attribute_generated": null
},
"allow_nil?": false,
"primary_key?": false,
"generated?": false
}
],
"table": "discord_messages",
"hash": "52BCA98DD8B99880A3BD22A6CF45EC177E9BE8F20FCE47D4D961EF056C0A2C06",
"repo": "Elixir.AshHq.Repo",
"schema": null,
"identities": [],
"custom_indexes": [],
"base_filter": null,
"custom_statements": [],
"multitenancy": {
"global": null,
"strategy": null,
"attribute": null
},
"has_create_action": true,
"check_constraints": []
}

View file

@ -192,4 +192,4 @@
},
"repo": "Elixir.AshHq.Repo",
"table": "dsls"
}
}

View file

@ -192,4 +192,4 @@
},
"repo": "Elixir.AshHq.Repo",
"table": "dsls"
}
}

View file

@ -219,4 +219,4 @@
"repo": "Elixir.AshHq.Repo",
"schema": null,
"table": "dsls"
}
}

View file

@ -239,4 +239,4 @@
"repo": "Elixir.AshHq.Repo",
"schema": null,
"table": "dsls"
}
}

View file

@ -249,4 +249,4 @@
"repo": "Elixir.AshHq.Repo",
"schema": null,
"table": "dsls"
}
}

View file

@ -249,4 +249,4 @@
"repo": "Elixir.AshHq.Repo",
"schema": null,
"table": "dsls"
}
}

View file

@ -249,4 +249,4 @@
"repo": "Elixir.AshHq.Repo",
"schema": null,
"table": "dsls"
}
}

View file

@ -249,4 +249,4 @@
"repo": "Elixir.AshHq.Repo",
"schema": null,
"table": "dsls"
}
}

View file

@ -269,4 +269,4 @@
"repo": "Elixir.AshHq.Repo",
"schema": null,
"table": "dsls"
}
}

View file

@ -292,4 +292,4 @@
"repo": "Elixir.AshHq.Repo",
"schema": null,
"table": "dsls"
}
}

View file

@ -282,4 +282,4 @@
"repo": "Elixir.AshHq.Repo",
"schema": null,
"table": "dsls"
}
}

View file

@ -285,4 +285,4 @@
"repo": "Elixir.AshHq.Repo",
"schema": null,
"table": "dsls"
}
}

View file

@ -285,4 +285,4 @@
"repo": "Elixir.AshHq.Repo",
"schema": null,
"table": "dsls"
}
}

View file

@ -298,4 +298,4 @@
"repo": "Elixir.AshHq.Repo",
"schema": null,
"table": "dsls"
}
}

View file

@ -301,4 +301,4 @@
"base_filter": null,
"check_constraints": [],
"has_create_action": true
}
}

View file

@ -301,4 +301,4 @@
}
],
"has_create_action": true
}
}

View file

@ -127,4 +127,4 @@
},
"repo": "Elixir.AshHq.Repo",
"table": "extensions"
}
}

View file

@ -127,4 +127,4 @@
},
"repo": "Elixir.AshHq.Repo",
"table": "extensions"
}
}

View file

@ -149,4 +149,4 @@
"repo": "Elixir.AshHq.Repo",
"schema": null,
"table": "extensions"
}
}

View file

@ -159,4 +159,4 @@
"repo": "Elixir.AshHq.Repo",
"schema": null,
"table": "extensions"
}
}

View file

@ -159,4 +159,4 @@
"repo": "Elixir.AshHq.Repo",
"schema": null,
"table": "extensions"
}
}

View file

@ -159,4 +159,4 @@
"repo": "Elixir.AshHq.Repo",
"schema": null,
"table": "extensions"
}
}

View file

@ -179,4 +179,4 @@
"repo": "Elixir.AshHq.Repo",
"schema": null,
"table": "extensions"
}
}

View file

@ -182,4 +182,4 @@
"repo": "Elixir.AshHq.Repo",
"schema": null,
"table": "extensions"
}
}

View file

@ -182,4 +182,4 @@
"repo": "Elixir.AshHq.Repo",
"schema": null,
"table": "extensions"
}
}

View file

@ -192,4 +192,4 @@
"repo": "Elixir.AshHq.Repo",
"schema": null,
"table": "extensions"
}
}

View file

@ -166,4 +166,4 @@
},
"repo": "Elixir.AshHq.Repo",
"table": "functions"
}
}

View file

@ -143,4 +143,4 @@
},
"repo": "Elixir.AshHq.Repo",
"table": "functions"
}
}

View file

@ -143,4 +143,4 @@
},
"repo": "Elixir.AshHq.Repo",
"table": "functions"
}
}

View file

@ -146,4 +146,4 @@
"repo": "Elixir.AshHq.Repo",
"schema": null,
"table": "functions"
}
}

View file

@ -166,4 +166,4 @@
"repo": "Elixir.AshHq.Repo",
"schema": null,
"table": "functions"
}
}

View file

@ -186,4 +186,4 @@
"repo": "Elixir.AshHq.Repo",
"schema": null,
"table": "functions"
}
}

View file

@ -196,4 +196,4 @@
"repo": "Elixir.AshHq.Repo",
"schema": null,
"table": "functions"
}
}

View file

@ -196,4 +196,4 @@
"repo": "Elixir.AshHq.Repo",
"schema": null,
"table": "functions"
}
}

View file

@ -209,4 +209,4 @@
"repo": "Elixir.AshHq.Repo",
"schema": null,
"table": "functions"
}
}

View file

@ -229,4 +229,4 @@
"repo": "Elixir.AshHq.Repo",
"schema": null,
"table": "functions"
}
}

View file

@ -232,4 +232,4 @@
"repo": "Elixir.AshHq.Repo",
"schema": null,
"table": "functions"
}
}

View file

@ -232,4 +232,4 @@
"repo": "Elixir.AshHq.Repo",
"schema": null,
"table": "functions"
}
}

Some files were not shown because too many files have changed in this diff Show more