Class: Assistant::MicrosoftAdsToolBuilder

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

Overview

Builds RubyLLM tools for Microsoft Advertising (Bing Ads) so the assistant
(Sunny) can read performance and — for an ads operator — change campaign
budget/status/MaxCpc, effective ad-group bids/status, and campaign negative
keywords.

Tools are hand-curated (like GoogleAdsToolBuilder) rather than discovered
from an upstream MCP catalog (like AdlabsToolBuilder), because Microsoft
ships no MCP surface — just the v13 REST API.

Read vs. write is role-gated

The mutating tools change a live ad account that spends real money, so
they are built only for accounts holding +microsoft_ad_specialist+ (or
admins). Everyone else with the +microsoft_ads+ service — notably
+marketing_rep+ — gets the read-only subset. This mirrors
AdlabsToolBuilder exactly; see config/analytics/data_domains.yml for the
role → service grants.

Unlike AdLabs there are no mixed-action tools here: each tool either reads
or writes, so the split is clean rather than conservative.

Constant Summary collapse

WRITE_TOOLS =

Tools that mutate the live Microsoft Ads account. Excluded from the
read-only build.

MicrosoftAdsChangeApproval::WRITE_TOOLS
APPROVAL_TOOL =
'microsoft_ads_request_change_approval'
PRIVILEGED_TOOLS =
[APPROVAL_TOOL, *WRITE_TOOLS].freeze

Class Method Summary collapse

Class Method Details

.client_credentialsObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Credentials are resolved per execution, not per build: the OAuth
access token is short-lived, and building a tool list must not make a
network call.



76
77
78
79
80
81
82
83
# File 'app/services/assistant/microsoft_ads_tool_builder.rb', line 76

def client_credentials
  {
    developer_token: Heatwave::Configuration.fetch(:microsoft_ads, :developer_token),
    access_token:    MicrosoftAds::OauthService.new.access_token!,
    customer_id:     Heatwave::Configuration.fetch(:microsoft_ads, :customer_id),
    account_id:      Heatwave::Configuration.fetch(:microsoft_ads, :account_id)
  }
end

.execute_approved(context:, tool_name:, payload:, approval_code:) ⇒ Object

The single mutation entry point used by every write tool. Approval is
consumed before OAuth credentials are resolved or a client is created.



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'app/services/assistant/microsoft_ads_tool_builder.rb', line 87

def execute_approved(context:, tool_name:, payload:, approval_code:)
  begin
    approval = MicrosoftAdsChangeApproval.consume(
      **context,
      tool_name:,
      payload:,
      approval_code:
    )
  rescue StandardError => e
    log_write_failure(tool_name:, context:, error: e)
    return {
      status: :failed,
      error: "#{tool_name} could not check its approval, so no change was attempted. " \
             'Retry with the same approval code.'
    }.to_json
  end
  return { status: :failed, error: approval.error }.to_json unless approval.approved?

  yield approval.payload.symbolize_keys
rescue StandardError => e
  log_write_failure(tool_name:, context:, error: e)
  {
    status: :failed,
    error: "#{tool_name} failed unexpectedly. The approval code was consumed. " \
           'Verify the current state with microsoft_ads_entities, then request a new approval code ' \
           'before attempting the change again.'
  }.to_json
end

.tools(account: nil, audit_context: {}) ⇒ Array<RubyLLM::Tool>

Write access is derived from +account+ and nothing else — there is
deliberately no write: override parameter. An override would be a
public bypass of the role gate on tools that spend real money, and a
caller passing write: true would silently defeat it. Tests stub
write_access? instead. (AdlabsToolBuilder still takes the override;
not replicated here.)

Parameters:

  • account (Account, nil) (defaults to: nil)

    CRM account; gates the write tools

  • audit_context (Hash) (defaults to: {})

    conversation ID used for write approval

Returns:

  • (Array<RubyLLM::Tool>)

    instantiated tools (empty on any failure)



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'app/services/assistant/microsoft_ads_tool_builder.rb', line 44

def tools(account: nil, audit_context: {})
  write = write_access?()

  list = [build_report_tool, build_entities_tool]
  if write
    context = approval_context(account:, audit_context:)
    list += [
      build_request_change_approval_tool(context),
      build_update_campaign_tool(context),
      build_update_ad_group_tool(context),
      build_add_negative_keywords_tool(context)
    ]
  end
  list
rescue StandardError => e
  Rails.logger.warn("[MicrosoftAdsToolBuilder] Failed to build Microsoft Ads tools: #{e.message}")
  []
end

.write_access?(account) ⇒ Boolean

True when +account+ may use the mutating Microsoft Ads tools.

Parameters:

Returns:

  • (Boolean)


66
67
68
69
70
# File 'app/services/assistant/microsoft_ads_tool_builder.rb', line 66

def write_access?()
  return false unless 

  .is_admin? || .has_role?('microsoft_ad_specialist', admin_check: false)
end