diff --git a/app/lib/meadow/chat/message.ex b/app/lib/meadow/chat/message.ex new file mode 100644 index 000000000..af7e5713a --- /dev/null +++ b/app/lib/meadow/chat/message.ex @@ -0,0 +1,11 @@ +defmodule Meadow.Chat.Message do + @moduledoc """ + Struct for chat messages + """ + + defstruct id: nil, + conversation_id: nil, + type: nil, + message: nil, + inserted_at: nil +end diff --git a/app/lib/meadow_web/resolvers/chat.ex b/app/lib/meadow_web/resolvers/chat.ex new file mode 100644 index 000000000..bf55d7359 --- /dev/null +++ b/app/lib/meadow_web/resolvers/chat.ex @@ -0,0 +1,24 @@ +defmodule MeadowWeb.Resolvers.Chat do + + @moduledoc """ + Absinthe GraphQL query resolver for Chat Context + + """ + + def send_chat_message(_, %{conversation_id: conversation_id, type: type, query: query, prompt: prompt}, _) do + response = %{ + conversation_id: conversation_id, + message: "Here is your plan....", + type: "plan", + } + + + Absinthe.Subscription.publish( + MeadowWeb.Endpoint, + response, + chat_response: "conversation:#{conversation_id}" + ) + + {:ok, %{conversation_id: conversation_id, type: type, query: query, prompt: prompt}} + end +end diff --git a/app/lib/meadow_web/schema/schema.ex b/app/lib/meadow_web/schema/schema.ex index 36b4123d5..52c15b34f 100644 --- a/app/lib/meadow_web/schema/schema.ex +++ b/app/lib/meadow_web/schema/schema.ex @@ -26,6 +26,7 @@ defmodule MeadowWeb.Schema do import_types(__MODULE__.HelperTypes) import_types(__MODULE__.Data.CSVMetadataUpdateTypes) import_types(__MODULE__.NULAuthorityTypes) + import_types(__MODULE__.ChatTypes) query do import_fields(:account_queries) @@ -53,10 +54,12 @@ defmodule MeadowWeb.Schema do import_fields(:nul_authority_mutations) import_fields(:shared_link_mutations) import_fields(:work_mutations) + import_fields(:chat_mutations) end subscription do import_fields(:ingest_subscriptions) + import_fields(:chat_subscriptions) end enum :sort_order do diff --git a/app/lib/meadow_web/schema/types/chat_types.ex b/app/lib/meadow_web/schema/types/chat_types.ex new file mode 100644 index 000000000..40e0b3791 --- /dev/null +++ b/app/lib/meadow_web/schema/types/chat_types.ex @@ -0,0 +1,54 @@ +defmodule MeadowWeb.Schema.ChatTypes do + @moduledoc """ + Absinthe schema for Chat and ChatMessage types + """ + use Absinthe.Schema.Notation + + alias MeadowWeb.Resolvers + alias MeadowWeb.Schema.Middleware + + object :chat_subscriptions do + field :chat_response, :chat_response do + arg :conversation_id, non_null(:id) + + config fn args, _ -> + {:ok, topic: "conversation:#{args.conversation_id}"} + end + end + end + + object :chat_mutations do + field :send_chat_message, type: :chat_message do + arg :conversation_id, non_null(:id) + arg :type, non_null(:string) + arg :query, non_null(:string) + arg :prompt, non_null(:string) + middleware(Middleware.Authenticate) + middleware(Middleware.Authorize, "Editor") + + resolve &Resolvers.Chat.send_chat_message/3 + end + end + + + + object :chat_message do + field :conversation_id, :id, + description: "Ref for the conversation" + field :type, :string, + description: "Type of message, e.g. 'chat'" + field :query, :string, + description: "The search query associated with the message" + field :prompt, :string, + description: "The prompt associated with the message" + end + + object :chat_response do + field :conversation_id, :id + field :type, :string, + description: "Type of message, e.g. 'chat'" + field :message, :string, + description: "AI response message" + end + +end diff --git a/app/test/gql/ChatResponse.gql b/app/test/gql/ChatResponse.gql new file mode 100644 index 000000000..a819089b0 --- /dev/null +++ b/app/test/gql/ChatResponse.gql @@ -0,0 +1,6 @@ +subscription ChatResponse($conversationId: ID!) { + chatResponse(conversationId: $conversationId) { + conversationId + message + } +} diff --git a/app/test/gql/SendChatMessage.gql b/app/test/gql/SendChatMessage.gql new file mode 100644 index 000000000..b736b090e --- /dev/null +++ b/app/test/gql/SendChatMessage.gql @@ -0,0 +1,18 @@ +mutation SendChatMessage( + $conversationId: ID! + $type: String! + $query: String! + $prompt: String! +) { + sendChatMessage( + conversationId: $conversationId + type: $type + query: $query + prompt: $prompt + ) { + conversationId + type + query + prompt + } +} diff --git a/app/test/meadow_web/schema/mutation/send_chat_message_test.exs b/app/test/meadow_web/schema/mutation/send_chat_message_test.exs new file mode 100644 index 000000000..028579057 --- /dev/null +++ b/app/test/meadow_web/schema/mutation/send_chat_message_test.exs @@ -0,0 +1,45 @@ +defmodule MeadowWeb.Schema.Mutation.SendChatMessageTest do + use Meadow.DataCase + use MeadowWeb.ConnCase, async: true + use Wormwood.GQLCase + + load_gql(MeadowWeb.Schema, "test/gql/SendChatMessage.gql") + + test "should send a chat message as editor" do + result = + query_gql( + variables: %{ + "conversationId" => "test-conversation-123", + "type" => "chat", + "query" => "test query", + "prompt" => "test prompt" + }, + context: gql_context(%{role: :editor}) + ) + + assert {:ok, query_data} = result + + message = get_in(query_data, [:data, "sendChatMessage"]) + + assert message["conversationId"] == "test-conversation-123" + assert message["type"] == "chat" + assert message["query"] == "test query" + assert message["prompt"] == "test prompt" + end + + test "should reject unauthorized users" do + result = + query_gql( + variables: %{ + "conversationId" => "test-conversation-123", + "type" => "chat", + "query" => "test query", + "prompt" => "test prompt" + }, + context: gql_context(%{role: :user}) + ) + + assert {:ok, query_data} = result + assert get_in(query_data, [:errors]) != nil + end +end diff --git a/app/test/meadow_web/schema/subscription/chat_response_test.exs b/app/test/meadow_web/schema/subscription/chat_response_test.exs new file mode 100644 index 000000000..6cb154c27 --- /dev/null +++ b/app/test/meadow_web/schema/subscription/chat_response_test.exs @@ -0,0 +1,14 @@ +defmodule MeadowWeb.Schema.Subscription.ChatResponseTest do + use Meadow.DataCase + use MeadowWeb.SubscriptionCase, async: true + + @reply_timeout 5000 + + load_gql(MeadowWeb.Schema, "test/gql/ChatResponse.gql") + + test "should initiate subscription as editor", %{socket: socket} do + conversation_id = "test-conversation-123" + ref = subscribe_gql(socket, variables: %{"conversationId" => conversation_id}, context: gql_context(%{role: :editor})) + assert_reply ref, :ok, %{subscriptionId: _subscription_id}, @reply_timeout + end +end