Class: GammaClient

Inherits:
Object
  • Object
show all
Defined in:
app/services/gamma_client.rb

Overview

Faraday-backed client for the Gamma Generate API.

Gamma is an AI-powered presentation/document/webpage creator.
Generations are asynchronous: POST /generations returns a generationId,
which is then polled via GET /generations/:id until status == "completed".

Credentials: stored in Rails credentials under gamma: { api_key: ... }

Defined Under Namespace

Classes: Response

Constant Summary collapse

BASE_URL =
'https://public-api.gamma.app/v1.0'
POLL_INTERVAL_SECONDS =

Polling configuration

4
DEFAULT_TIMEOUT_SECONDS =
120

Instance Method Summary collapse

Constructor Details

#initialize(api_key: nil) ⇒ GammaClient

Returns a new instance of GammaClient.

Raises:

  • (ArgumentError)


25
26
27
28
29
30
# File 'app/services/gamma_client.rb', line 25

def initialize(api_key: nil)
  @api_key = api_key || Heatwave::Configuration.fetch(:gamma, :api_key)
  raise ArgumentError, 'Gamma API key not configured' if @api_key.blank?

  @connection = build_connection
end

Instance Method Details

#create_and_wait(timeout: DEFAULT_TIMEOUT_SECONDS, **generation_params) ⇒ Response

Submit a generation and poll until it completes or times out.
Suitable for synchronous use inside a RubyLLM tool call.

Parameters:

  • timeout (Integer) (defaults to: DEFAULT_TIMEOUT_SECONDS)

    maximum seconds to wait (default: 120)

Returns:

  • (Response)

    with data: { gamma_url:, export_url:, credits: }



311
312
313
314
315
316
# File 'app/services/gamma_client.rb', line 311

def create_and_wait(timeout: DEFAULT_TIMEOUT_SECONDS, **generation_params)
  submit = create_generation(**generation_params)
  return submit unless submit.success?

  poll_until_complete(submit.data[:generation_id], timeout: timeout)
end

#create_from_template(gamma_id:, prompt:, theme_id: nil, folder_ids: nil, export_as: nil, image_options: nil, sharing_options: nil) ⇒ Response

Rework an existing gamma using it as a template.
Creates a NEW gamma based on the original — the original is not modified.

Parameters:

  • gamma_id (String)

    The gammaId of the existing gamma to use as a template.
    Find this in the Gamma app URL (e.g. https://gamma.app/docs/my-deck-abcdef123456)

  • prompt (String)

    Instructions for how to adapt the template. Required.
    E.g. "Rework this pitch deck for a non-technical audience." or
    "Convert this presentation into a formal document with detailed prose."

  • theme_id (String, nil) (defaults to: nil)

    Override the template's theme. Defaults to the template's own theme.

  • folder_ids (Array<String>) (defaults to: nil)

    Folders to save the new gamma into.

  • export_as (String, nil) (defaults to: nil)

    "pdf" or "pptx"

  • image_options (Hash, nil) (defaults to: nil)

    { model:, style: } — only applies when template used AI images

  • sharing_options (Hash, nil) (defaults to: nil)

    { workspaceAccess:, externalAccess: }

Returns:

  • (Response)

    with data: { generation_id: }



256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
# File 'app/services/gamma_client.rb', line 256

def create_from_template(
  gamma_id:,
  prompt:,
  theme_id: nil,
  folder_ids: nil,
  export_as: nil,
  image_options: nil,
  sharing_options: nil
)
  payload = { gammaId: gamma_id, prompt: prompt }
  payload[:themeId]        = theme_id       if theme_id.present?
  payload[:folderIds]      = folder_ids     if folder_ids.present?
  payload[:exportAs]       = export_as      if export_as.present?
  payload[:imageOptions]   = image_options  if image_options.present?
  payload[:sharingOptions] = sharing_options if sharing_options.present?

  Rails.logger.info("[GammaClient] Creating from template gammaId=#{gamma_id}")

  response = @connection.post('generations/from-template') do |req|
    req.body = payload
  end

  if response.success?
    generation_id = response.body['generationId']
    Rails.logger.info("[GammaClient] Template generation submitted: #{generation_id}")
    Response.new(success: true, data: { generation_id: generation_id }, error: nil)
  else
    error = extract_error(response)
    Rails.logger.error("[GammaClient] Create from template failed (#{response.status}): #{error}")
    Response.new(success: false, data: nil, error: error)
  end
rescue Faraday::TimeoutError
  Response.new(success: false, data: nil, error: 'Gamma API request timed out')
rescue Faraday::Error => e
  Rails.logger.error("[GammaClient] Connection error: #{e.message}")
  Response.new(success: false, data: nil, error: "Connection error: #{e.message}")
end

#create_generation(input_text:, text_mode: 'generate', format: 'presentation', theme_id: nil, num_cards: nil, card_split: 'auto', additional_instructions: nil, folder_ids: nil, export_as: nil, text_options: nil, image_options: nil, card_options: nil, sharing_options: nil) ⇒ Response

Submit a new generation request.

Parameters:

  • input_text (String)

    The prompt or content to generate from. Required.

  • text_mode (String) (defaults to: 'generate')

    "generate", "condense", or "preserve"

  • format (String) (defaults to: 'presentation')

    "presentation" (default), "document", "social", "webpage"

  • theme_id (String, nil) (defaults to: nil)

    Theme ID from list_themes (defaults to workspace default)

  • num_cards (Integer, nil) (defaults to: nil)

    Number of slides/cards (1–75)

  • card_split (String) (defaults to: 'auto')

    "auto" (default) or "inputTextBreaks"

  • additional_instructions (String, nil) (defaults to: nil)

    Extra guidance (max 2000 chars)

  • folder_ids (Array<String>) (defaults to: nil)

    Folder IDs to save the gamma into

  • export_as (String, nil) (defaults to: nil)

    "pdf" or "pptx" for a download link in the response

  • text_options (Hash, nil) (defaults to: nil)

    { amount:, tone:, audience:, language: }

  • image_options (Hash, nil) (defaults to: nil)

    { source:, model:, style: }

  • card_options (Hash, nil) (defaults to: nil)

    { dimensions:, headerFooter: }

  • sharing_options (Hash, nil) (defaults to: nil)

    { workspaceAccess:, externalAccess: }

Returns:

  • (Response)

    with data: { generation_id: }



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'app/services/gamma_client.rb', line 48

def create_generation(
  input_text:,
  text_mode: 'generate',
  format: 'presentation',
  theme_id: nil,
  num_cards: nil,
  card_split: 'auto',
  additional_instructions: nil,
  folder_ids: nil,
  export_as: nil,
  text_options: nil,
  image_options: nil,
  card_options: nil,
  sharing_options: nil
)
  payload = {
    inputText: input_text,
    textMode: text_mode,
    format: format,
    cardSplit: card_split
  }

  payload[:themeId]               = theme_id               if theme_id.present?
  payload[:numCards]              = num_cards               if num_cards.present?
  payload[:additionalInstructions] = additional_instructions if additional_instructions.present?
  payload[:folderIds]             = folder_ids              if folder_ids.present?
  payload[:exportAs]              = export_as               if export_as.present?
  payload[:textOptions]           = text_options            if text_options.present?
  payload[:imageOptions]          = image_options           if image_options.present?
  payload[:cardOptions]           = card_options            if card_options.present?
  payload[:sharingOptions]        = sharing_options         if sharing_options.present?

  Rails.logger.info("[GammaClient] Creating generation: format=#{format} textMode=#{text_mode}")

  response = @connection.post('generations') do |req|
    req.body = payload
  end

  if response.success?
    generation_id = response.body['generationId']
    Rails.logger.info("[GammaClient] Generation submitted: #{generation_id}")
    Response.new(success: true, data: { generation_id: generation_id }, error: nil)
  else
    error = extract_error(response)
    Rails.logger.error("[GammaClient] Create failed (#{response.status}): #{error}")
    Response.new(success: false, data: nil, error: error)
  end
rescue Faraday::TimeoutError
  Response.new(success: false, data: nil, error: 'Gamma API request timed out')
rescue Faraday::Error => e
  Rails.logger.error("[GammaClient] Connection error: #{e.message}")
  Response.new(success: false, data: nil, error: "Connection error: #{e.message}")
end

#fetch_pdf_url(gamma_id:) ⇒ Response

Fetch a PDF export URL for any gamma, identified by its gammaId or generationId.

Strategy:

  1. Call GET /generations/id — if the original generation included exportAs: "pdf",
    the export_url is already available.
  2. If not (gamma was created without export, or this is a gammaId), create a PDF
    export via create_from_template with a content-preserving prompt.

Parameters:

  • gamma_id (String)

    gammaId from the Gamma URL, OR a prior generationId

Returns:

  • (Response)

    with data: { pdf_url: }



210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'app/services/gamma_client.rb', line 210

def fetch_pdf_url(gamma_id:)
  # Step 1: check if a previous generation already has an export URL
  existing = get_generation(gamma_id)
  if existing.success? && existing.data[:export_url].present?
    Rails.logger.info("[GammaClient] Using cached export URL for #{gamma_id}")
    return Response.new(success: true, data: { pdf_url: existing.data[:export_url] }, error: nil)
  end

  # Step 2: generate a new PDF export using the gamma as a template.
  # This only works if the gamma is marked as a "Template" in the Gamma workspace.
  Rails.logger.info("[GammaClient] No cached PDF for #{gamma_id} — attempting PDF export via template")
  result = rework_and_wait(
    gamma_id:  gamma_id,
    prompt:    'Export this as a PDF preserving all content exactly as-is.',
    export_as: 'pdf'
  )

  if result.success? && result.data[:export_url].present?
    Response.new(success: true, data: { pdf_url: result.data[:export_url] }, error: nil)
  elsif result.success?
    Response.new(success: false, data: nil, error: 'PDF export was not returned by Gamma. Try specifying export_as: "pdf" when creating the presentation.')
  elsif result.error.to_s.downcase.include?('access denied') || result.error.to_s.downcase.include?('template')
    Response.new(
      success: false,
      data:    nil,
      error:   'TEMPLATE_ACCESS_DENIED'
    )
  else
    result
  end
end

#get_generation(generation_id) ⇒ Response

Poll for the status of a generation.

Parameters:

  • generation_id (String)

Returns:

  • (Response)

    with data: { status:, gamma_url:, export_url:, credits: }



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'app/services/gamma_client.rb', line 106

def get_generation(generation_id)
  response = @connection.get("generations/#{generation_id}")

  if response.success?
    body = response.body
    data = {
      status:     body['status'],
      gamma_url:  body['gammaUrl'],
      export_url: body['exportUrl'],
      credits:    body['credits']
    }.compact
    Response.new(success: true, data: data, error: nil)
  else
    error = extract_error(response)
    Rails.logger.error("[GammaClient] Poll failed (#{response.status}): #{error}")
    Response.new(success: false, data: nil, error: error)
  end
rescue Faraday::TimeoutError
  Response.new(success: false, data: nil, error: 'Gamma API poll timed out')
rescue Faraday::Error => e
  Response.new(success: false, data: nil, error: "Connection error: #{e.message}")
end

#list_folders(query: nil, limit: 50, after: nil) ⇒ Response

Retrieve folders in your workspace.
Supports cursor-based pagination and name search.

Parameters:

  • query (String, nil) (defaults to: nil)

    search by folder name

  • limit (Integer) (defaults to: 50)

    items per page (max 50)

  • after (String, nil) (defaults to: nil)

    cursor from a previous response

Returns:

  • (Response)

    with data: { folders: [{ id:, name: }], has_more:, next_cursor: }



176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'app/services/gamma_client.rb', line 176

def list_folders(query: nil, limit: 50, after: nil)
  Rails.logger.info('[GammaClient] Listing folders')
  response = @connection.get('folders') do |req|
    req.params['query'] = query if query.present?
    req.params['limit'] = limit
    req.params['after'] = after if after.present?
  end

  if response.success?
    body    = response.body
    folders = Array(body['data']).map { |f| { id: f['id'], name: f['name'] }.compact }
    Response.new(success: true, data: {
      folders:     folders,
      has_more:    body['hasMore'],
      next_cursor: body['nextCursor']
    }, error: nil)
  else
    error = extract_error(response)
    Response.new(success: false, data: nil, error: error)
  end
rescue Faraday::Error => e
  Response.new(success: false, data: nil, error: "Connection error: #{e.message}")
end

#list_themes(query: nil, limit: 50, after: nil) ⇒ Response

Retrieve available themes in your workspace.
Supports cursor-based pagination and name search.

Parameters:

  • query (String, nil) (defaults to: nil)

    search by theme name (case-insensitive)

  • limit (Integer) (defaults to: 50)

    items per page (max 50)

  • after (String, nil) (defaults to: nil)

    cursor from a previous response for next-page fetching

Returns:

  • (Response)

    with data: { themes: [{ id:, name:, type:, color_keywords:, tone_keywords: }], has_more:, next_cursor: }



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'app/services/gamma_client.rb', line 136

def list_themes(query: nil, limit: 50, after: nil)
  Rails.logger.info('[GammaClient] Listing themes')
  response = @connection.get('themes') do |req|
    req.params['query'] = query if query.present?
    req.params['limit'] = limit
    req.params['after'] = after if after.present?
  end

  if response.success?
    body   = response.body
    themes = Array(body['data']).map do |t|
      {
        id:             t['id'],
        name:           t['name'],
        type:           t['type'],
        color_keywords: t['colorKeywords'],
        tone_keywords:  t['toneKeywords']
      }.compact
    end
    Response.new(success: true, data: {
      themes:      themes,
      has_more:    body['hasMore'],
      next_cursor: body['nextCursor']
    }, error: nil)
  else
    error = extract_error(response)
    Rails.logger.error("[GammaClient] List themes failed (#{response.status}): #{error}")
    Response.new(success: false, data: nil, error: error)
  end
rescue Faraday::Error => e
  Response.new(success: false, data: nil, error: "Connection error: #{e.message}")
end

#rework_and_wait(timeout: DEFAULT_TIMEOUT_SECONDS, **template_params) ⇒ Response

Rework an existing gamma and wait for the new one to complete.
Wraps create_from_template + polling. Suitable for use inside a tool call.

Parameters:

  • timeout (Integer) (defaults to: DEFAULT_TIMEOUT_SECONDS)

    maximum seconds to wait (default: 120)

Returns:

  • (Response)

    with data: { gamma_url:, export_url:, credits: }



299
300
301
302
303
304
# File 'app/services/gamma_client.rb', line 299

def rework_and_wait(timeout: DEFAULT_TIMEOUT_SECONDS, **template_params)
  submit = create_from_template(**template_params)
  return submit unless submit.success?

  poll_until_complete(submit.data[:generation_id], timeout: timeout)
end