lib/openagents/voice_sessions.ex

58e6347eeb72 · 2 KB

defmodule OpenAgents.VoiceSessions do
  @moduledoc "Supervised entry point for admitting and controlling live voice sessions."

  alias OpenAgents.Conversations.{Conversation, Message}
  alias OpenAgents.Voice
  alias OpenAgents.Voice.{CallAdmission, Config, Session}
  alias OpenAgents.VoiceSessions.SessionServer

  @spec connect(Conversation.t(), String.t(), String.t(), Config.t()) ::
          {:ok, Session.t(), CallAdmission.t()} | {:error, term()}
  def connect(
        %Conversation{} = conversation,
        sdp_offer,
        safety_identifier,
        %Config{} = config
      ) do
    with {:ok, session} <- Voice.admit_session(conversation, config),
         {:ok, process} <- start(session.id) do
      try do
        case GenServer.call(process, {:connect, sdp_offer, safety_identifier}, 35_000) do
          {:ok, %CallAdmission{} = admission} ->
            {:ok, Voice.get_session!(session.id), admission}

          {:error, reason} ->
            {:error, reason}
        end
      catch
        :exit, _reason ->
          _failure_result = Voice.fail_session(session, session.generation, :connect_timeout)
          {:error, :voice_connection_failed}
      end
    end
  end

  @spec start(Ecto.UUID.t()) :: DynamicSupervisor.on_start_child()
  def start(session_id) do
    DynamicSupervisor.start_child(OpenAgents.VoiceSessionSupervisor, {SessionServer, session_id})
  end

  @spec end_session(Session.t(), String.t()) :: {:ok, Session.t()} | {:error, term()}
  def end_session(%Session{} = session, reason \\ "user_ended") do
    case whereis(session.id) do
      nil -> Voice.end_session(session, session.generation, reason)
      process -> GenServer.call(process, {:end_session, reason})
    end
  end

  @spec interrupt_session(Session.t()) :: {:ok, Session.t()} | {:error, term()}
  def interrupt_session(%Session{} = session) do
    case whereis(session.id) do
      nil -> {:error, :voice_session_not_running}
      process -> GenServer.call(process, :interrupt_response)
    end
  end

  @doc """
  Adds a typed user message to the live provider conversation so voice Sarah
  can read it (links, code, exact text) without ending the call.
  """
  @spec inject_typed_message(Session.t(), Message.t()) :: :ok | {:error, term()}
  def inject_typed_message(%Session{} = session, %Message{} = message) do
    case whereis(session.id) do
      nil -> {:error, :voice_session_not_running}
      process -> GenServer.call(process, {:inject_typed_message, message.content})
    end
  end

  @spec send_control(Session.t(), map()) :: :ok | {:error, term()}
  def send_control(%Session{} = session, event) when is_map(event) do
    case whereis(session.id) do
      nil -> {:error, :voice_session_not_running}
      process -> GenServer.call(process, {:send_control, event})
    end
  end

  @spec whereis(Ecto.UUID.t()) :: pid() | nil
  def whereis(session_id) do
    case Registry.lookup(OpenAgents.VoiceSessionRegistry, session_id) do
      [{process, _value}] -> process
      [] -> nil
    end
  end
end