lib/mix/tasks/openagents.atif.export.ex

436d37facc96 · 2 KB

defmodule Mix.Tasks.Openagents.Atif.Export do
  @shortdoc "Export one account's conversation as an ATIF v1.7 trajectory file"

  @moduledoc """
  Exports the same ATIF document as `GET /data/export/atif` without starting
  the OpenAgents supervision tree or its recovery workers.

      mix openagents.atif.export --github-id 14167547 --out /tmp/atif.json \
        --database-url postgres://openagents_app:URL_ENCODED_PASSWORD@127.0.0.1:5433/openagents
  """

  use Mix.Task

  import Ecto.Query

  @requirements ["app.config"]

  @impl Mix.Task
  def run(arguments) do
    {options, remaining, invalid} =
      OptionParser.parse(arguments,
        strict: [github_id: :integer, out: :string, database_url: :string]
      )

    if remaining != [] or invalid != [], do: Mix.raise("invalid ATIF export options")

    github_id =
      Keyword.get(options, :github_id) || Mix.raise("--github-id GITHUB_NUMERIC_ID is required")

    start_repo_only(Keyword.get(options, :database_url))

    user =
      OpenAgents.Repo.get_by(OpenAgents.Accounts.User, github_id: github_id) ||
        Mix.raise("no user with GitHub ID #{github_id}")

    conversation =
      OpenAgents.Repo.one(
        from(conversation in OpenAgents.Conversations.Conversation,
          join: visitor in OpenAgents.Conversations.Visitor,
          on: visitor.id == conversation.visitor_id,
          where: visitor.user_id == ^user.id,
          order_by: [asc: conversation.inserted_at],
          limit: 1
        )
      ) || Mix.raise("user #{github_id} has no conversation")

    visitor = OpenAgents.Repo.get!(OpenAgents.Conversations.Visitor, conversation.visitor_id)
    {:ok, export} = OpenAgents.DataRights.AtifExport.build(user, visitor, conversation)
    output = Keyword.get(options, :out, "openagents-conversation-#{conversation.id}-atif.json")
    File.write!(output, Jason.encode!(export, pretty: true))

    steps = export["steps"]
    tool_calls = steps |> Enum.flat_map(&(&1["tool_calls"] || [])) |> length()

    Mix.shell().info(
      "wrote #{output}: #{length(steps)} steps, #{tool_calls} tool calls, " <>
        "final_metrics=#{Jason.encode!(export["final_metrics"])}"
    )
  end

  defp start_repo_only(database_url) do
    {:ok, _apps} = Application.ensure_all_started(:ssl)
    {:ok, _apps} = Application.ensure_all_started(:postgrex)
    {:ok, _apps} = Application.ensure_all_started(:ecto_sql)
    _snapshot = OpenAgents.Tools.Registry.install!(Application.fetch_env!(:openagents, :tools))

    repo_options =
      case database_url do
        nil -> [pool_size: 2]
        url when is_binary(url) -> [url: url, pool_size: 2, ssl: false]
      end

    case OpenAgents.Repo.start_link(repo_options) do
      {:ok, _repo} -> :ok
      {:error, {:already_started, _repo}} -> :ok
      {:error, reason} -> Mix.raise("repo start failed: #{inspect(reason)}")
    end
  end
end