Class: Assistant::MicrosoftAdsToolBuilder
- Inherits:
-
Object
- Object
- Assistant::MicrosoftAdsToolBuilder
- 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
-
.client_credentials ⇒ Object
private
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.
-
.execute_approved(context:, tool_name:, payload:, approval_code:) ⇒ Object
The single mutation entry point used by every write tool.
-
.tools(account: nil, audit_context: {}) ⇒ Array<RubyLLM::Tool>
Write access is derived from +account+ and nothing else — there is deliberately no
write:override parameter. -
.write_access?(account) ⇒ Boolean
True when +account+ may use the mutating Microsoft Ads tools.
Class Method Details
.client_credentials ⇒ Object
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.)
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?(account) 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.}") [] end |
.write_access?(account) ⇒ Boolean
True when +account+ may use the mutating Microsoft Ads tools.
66 67 68 69 70 |
# File 'app/services/assistant/microsoft_ads_tool_builder.rb', line 66 def write_access?(account) return false unless account account.is_admin? || account.has_role?('microsoft_ad_specialist', admin_check: false) end |