Files
a2a-platform f9b16d05a1 deploy
2026-06-27 01:03:18 +00:00

679 lines
49 KiB
Python

from __future__ import annotations
import base64
import json
import re
from pathlib import Path
from typing import Any
import httpx
from a2a_pack.deepagents import create_a2a_deep_agent
from langchain_core.messages import BaseMessage
from langchain_core.tools import StructuredTool
from pydantic import BaseModel, Field
from a2a_pack import (
A2AAgent,
ConsumerSetup,
ConsumerSetupField,
ConsumerSetupMissing,
EgressPolicy,
LLMProvisioning,
Pricing,
Resources,
RunContext,
skill,
)
DEFAULT_BASE_URL = "https://api.hubapi.com"
OPERATIONS = json.loads("{\n \"delete_crm_v3_objects_contacts_contact_id_archive\": {\n \"base_url\": \"https://api.hubapi.com\",\n \"base_url_field\": \"OPENAPI_BASE_URL\",\n \"description\": \"Delete a contact by ID. Deleted contacts can be restored within 90 days of deletion. Learn more about the [data impacted by contact deletions](https://knowledge.hubspot.com/privacy-and-consent/understand-restorable-and-permanent-contact-deletions) and how to [restore archived records](https://knowledge.hubspot.com/records/restore-deleted-records).\",\n \"destructive\": true,\n \"method\": \"DELETE\",\n \"operation_id\": \"delete_crm_v3_objects_contacts_contact_id_archive\",\n \"parameters\": [\n {\n \"description\": \"\",\n \"in\": \"path\",\n \"name\": \"contactId\",\n \"required\": true,\n \"schema\": {\n \"example\": null,\n \"pattern\": \".+\",\n \"type\": \"string\"\n }\n }\n ],\n \"path\": \"/crm/v3/objects/contacts/{contactId}\",\n \"request_body\": null,\n \"security\": [\n {\n \"oauth2\": [\n \"crm.objects.contacts.write\"\n ]\n }\n ],\n \"skill_name\": \"delete_crm_v3_objects_contacts_contact_id_archive\",\n \"source_index\": 1,\n \"source_openapi_url\": \"https://raw.githubusercontent.com/HubSpot/HubSpot-public-api-spec-collection/main/PublicApiSpecs/CRM/Contacts/Rollouts/424/v3/contacts.json\",\n \"source_title\": \"Contacts\",\n \"summary\": \"Archive a contact\",\n \"tags\": [\n \"Basic\"\n ]\n },\n \"get_crm_v3_objects_contacts_contact_id_get_by_id\": {\n \"base_url\": \"https://api.hubapi.com\",\n \"base_url_field\": \"OPENAPI_BASE_URL\",\n \"description\": \"Retrieve a contact by its ID (`contactId`) or by a unique property (`idProperty`). You can specify what is returned using the `properties` query parameter.\",\n \"destructive\": false,\n \"method\": \"GET\",\n \"operation_id\": \"get_crm_v3_objects_contacts_contact_id_get_by_id\",\n \"parameters\": [\n {\n \"description\": \"\",\n \"in\": \"path\",\n \"name\": \"contactId\",\n \"required\": true,\n \"schema\": {\n \"example\": null,\n \"pattern\": \".+\",\n \"type\": \"string\"\n }\n },\n {\n \"description\": \"Whether to return only results that have been archived.\",\n \"in\": \"query\",\n \"name\": \"archived\",\n \"required\": false,\n \"schema\": {\n \"default\": false,\n \"example\": null,\n \"type\": \"boolean\"\n }\n },\n {\n \"description\": \"A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored.\",\n \"in\": \"query\",\n \"name\": \"associations\",\n \"required\": false,\n \"schema\": {\n \"example\": null,\n \"items\": {\n \"example\": null,\n \"type\": \"string\"\n },\n \"type\": \"array\"\n }\n },\n {\n \"description\": \"The name of a property whose values are unique for this object type\",\n \"in\": \"query\",\n \"name\": \"idProperty\",\n \"required\": false,\n \"schema\": {\n \"example\": null,\n \"type\": \"string\"\n }\n },\n {\n \"description\": \"A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored.\",\n \"in\": \"query\",\n \"name\": \"properties\",\n \"required\": false,\n \"schema\": {\n \"example\": null,\n \"items\": {\n \"example\": null,\n \"type\": \"string\"\n },\n \"type\": \"array\"\n }\n },\n {\n \"description\": \"A comma separated list of the properties to be returned along with their history of previous values. If any of the specified properties are not present on the requested object(s), they will be ignored.\",\n \"in\": \"query\",\n \"name\": \"propertiesWithHistory\",\n \"required\": false,\n \"schema\": {\n \"example\": null,\n \"items\": {\n \"example\": null,\n \"type\": \"string\"\n },\n \"type\": \"array\"\n }\n }\n ],\n \"path\": \"/crm/v3/objects/contacts/{contactId}\",\n \"request_body\": null,\n \"security\": [\n {\n \"oauth2\": [\n \"crm.objects.contacts.read\"\n ]\n }\n ],\n \"skill_name\": \"get_crm_v3_objects_contacts_contact_id_get_by_id\",\n \"source_index\": 1,\n \"source_openapi_url\": \"https://raw.githubusercontent.com/HubSpot/HubSpot-public-api-spec-collection/main/PublicApiSpecs/CRM/Contacts/Rollouts/424/v3/contacts.json\",\n \"source_title\": \"Contacts\",\n \"summary\": \"Retrieve a contact\",\n \"tags\": [\n \"Basic\"\n ]\n },\n \"get_crm_v3_objects_contacts_get_page\": {\n \"base_url\": \"https://api.hubapi.com\",\n \"base_url_field\": \"OPENAPI_BASE_URL\",\n \"description\": \"Retrieve all contacts, using query parameters to specify the information that gets returned.\",\n \"destructive\": false,\n \"method\": \"GET\",\n \"operation_id\": \"get_crm_v3_objects_contacts_get_page\",\n \"parameters\": [\n {\n \"description\": \"The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results.\",\n \"in\": \"query\",\n \"name\": \"after\",\n \"required\": false,\n \"schema\": {\n \"example\": null,\n \"type\": \"string\"\n }\n },\n {\n \"description\": \"Whether to return only results that have been archived.\",\n \"in\": \"query\",\n \"name\": \"archived\",\n \"required\": false,\n \"schema\": {\n \"default\": false,\n \"example\": null,\n \"type\": \"boolean\"\n }\n },\n {\n \"description\": \"A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored.\",\n \"in\": \"query\",\n \"name\": \"associations\",\n \"required\": false,\n \"schema\": {\n \"example\": null,\n \"items\": {\n \"example\": null,\n \"type\": \"string\"\n },\n \"type\": \"array\"\n }\n },\n {\n \"description\": \"The maximum number of results to display per page.\",\n \"in\": \"query\",\n \"name\": \"limit\",\n \"required\": false,\n \"schema\": {\n \"default\": 10,\n \"example\": null,\n \"format\": \"int32\",\n \"type\": \"integer\"\n }\n },\n {\n \"description\": \"A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored.\",\n \"in\": \"query\",\n \"name\": \"properties\",\n \"required\": false,\n \"schema\": {\n \"example\": null,\n \"items\": {\n \"example\": null,\n \"type\": \"string\"\n },\n \"type\": \"array\"\n }\n },\n {\n \"description\": \"A comma separated list of the properties to be returned along with their history of previous values. If any of the specified properties are not present on the requested object(s), they will be ignored. Usage of this parameter will reduce the maximum number of contacts that can be read by a single request.\",\n \"in\": \"query\",\n \"name\": \"propertiesWithHistory\",\n \"required\": false,\n \"schema\": {\n \"example\": null,\n \"items\": {\n \"example\": null,\n \"type\": \"string\"\n },\n \"type\": \"array\"\n }\n }\n ],\n \"path\": \"/crm/v3/objects/contacts\",\n \"request_body\": null,\n \"security\": [\n {\n \"oauth2\": [\n \"crm.objects.contacts.read\"\n ]\n }\n ],\n \"skill_name\": \"get_crm_v3_objects_contacts_get_page\",\n \"source_index\": 1,\n \"source_openapi_url\": \"https://raw.githubusercontent.com/HubSpot/HubSpot-public-api-spec-collection/main/PublicApiSpecs/CRM/Contacts/Rollouts/424/v3/contacts.json\",\n \"source_title\": \"Contacts\",\n \"summary\": \"Retrieve contacts\",\n \"tags\": [\n \"Basic\"\n ]\n },\n \"patch_crm_v3_objects_contacts_contact_id_update\": {\n \"base_url\": \"https://api.hubapi.com\",\n \"base_url_field\": \"OPENAPI_BASE_URL\",\n \"description\": \"Update an existing contact, identified by ID or email/unique property value. To identify a contact by ID, include the ID in the request URL path. To identify a contact by their email or other unique property, include the email/property value in the request URL path, and add the `idProperty` query parameter (`/crm/v3/objects/contacts/jon@website.com?idProperty=email`). Provided property values will be overwritten. Read-only and non-existent properties will result in an error. Properties values can be cleared by passing an empty string.\",\n \"destructive\": true,\n \"method\": \"PATCH\",\n \"operation_id\": \"patch_crm_v3_objects_contacts_contact_id_update\",\n \"parameters\": [\n {\n \"description\": \"\",\n \"in\": \"path\",\n \"name\": \"contactId\",\n \"required\": true,\n \"schema\": {\n \"example\": null,\n \"pattern\": \".+\",\n \"type\": \"string\"\n }\n },\n {\n \"description\": \"The name of a property whose values are unique for this object type\",\n \"in\": \"query\",\n \"name\": \"idProperty\",\n \"required\": false,\n \"schema\": {\n \"example\": null,\n \"type\": \"string\"\n }\n }\n ],\n \"path\": \"/crm/v3/objects/contacts/{contactId}\",\n \"request_body\": {\n \"$ref\": \"#/components/schemas/SimplePublicObjectInput\"\n },\n \"security\": [\n {\n \"oauth2\": [\n \"crm.objects.contacts.write\"\n ]\n }\n ],\n \"skill_name\": \"patch_crm_v3_objects_contacts_contact_id_update\",\n \"source_index\": 1,\n \"source_openapi_url\": \"https://raw.githubusercontent.com/HubSpot/HubSpot-public-api-spec-collection/main/PublicApiSpecs/CRM/Contacts/Rollouts/424/v3/contacts.json\",\n \"source_title\": \"Contacts\",\n \"summary\": \"Update a contact\",\n \"tags\": [\n \"Basic\"\n ]\n },\n \"post_crm_v3_objects_contacts_batch_archive_archive\": {\n \"base_url\": \"https://api.hubapi.com\",\n \"base_url_field\": \"OPENAPI_BASE_URL\",\n \"description\": \"Archive a batch of contacts by ID. Archived contacts can be restored within 90 days of deletion. Learn more about the [data impacted by contact deletions](https://knowledge.hubspot.com/privacy-and-consent/understand-restorable-and-permanent-contact-deletions) and how to [restore archived records](https://knowledge.hubspot.com/records/restore-deleted-records).\",\n \"destructive\": true,\n \"method\": \"POST\",\n \"operation_id\": \"post_crm_v3_objects_contacts_batch_archive_archive\",\n \"parameters\": [],\n \"path\": \"/crm/v3/objects/contacts/batch/archive\",\n \"request_body\": {\n \"$ref\": \"#/components/schemas/BatchInputSimplePublicObjectId\"\n },\n \"security\": [\n {\n \"oauth2\": [\n \"crm.objects.contacts.write\"\n ]\n }\n ],\n \"skill_name\": \"post_crm_v3_objects_contacts_batch_archive_archive\",\n \"source_index\": 1,\n \"source_openapi_url\": \"https://raw.githubusercontent.com/HubSpot/HubSpot-public-api-spec-collection/main/PublicApiSpecs/CRM/Contacts/Rollouts/424/v3/contacts.json\",\n \"source_title\": \"Contacts\",\n \"summary\": \"Archive a batch of contacts\",\n \"tags\": [\n \"Batch\"\n ]\n },\n \"post_crm_v3_objects_contacts_batch_create_create\": {\n \"base_url\": \"https://api.hubapi.com\",\n \"base_url_field\": \"OPENAPI_BASE_URL\",\n \"description\": \"Create a batch of contacts. The `inputs` array can contain a `properties` object to define property values for each record, along with an `associations` array to define [associations](https://developers.hubspot.com/docs/guides/api/crm/associations/associations-v4) with other CRM records.\",\n \"destructive\": true,\n \"method\": \"POST\",\n \"operation_id\": \"post_crm_v3_objects_contacts_batch_create_create\",\n \"parameters\": [],\n \"path\": \"/crm/v3/objects/contacts/batch/create\",\n \"request_body\": {\n \"$ref\": \"#/components/schemas/BatchInputSimplePublicObjectBatchInputForCreate\"\n },\n \"security\": [\n {\n \"oauth2\": [\n \"crm.objects.contacts.write\"\n ]\n }\n ],\n \"skill_name\": \"post_crm_v3_objects_contacts_batch_create_create\",\n \"source_index\": 1,\n \"source_openapi_url\": \"https://raw.githubusercontent.com/HubSpot/HubSpot-public-api-spec-collection/main/PublicApiSpecs/CRM/Contacts/Rollouts/424/v3/contacts.json\",\n \"source_title\": \"Contacts\",\n \"summary\": \"Create a batch of contacts\",\n \"tags\": [\n \"Batch\"\n ]\n },\n \"post_crm_v3_objects_contacts_batch_read_read\": {\n \"base_url\": \"https://api.hubapi.com\",\n \"base_url_field\": \"OPENAPI_BASE_URL\",\n \"description\": \"Retrieve a batch of contacts by ID (`contactId`) or unique property value (`idProperty`). \",\n \"destructive\": true,\n \"method\": \"POST\",\n \"operation_id\": \"post_crm_v3_objects_contacts_batch_read_read\",\n \"parameters\": [\n {\n \"description\": \"Whether to return only results that have been archived.\",\n \"in\": \"query\",\n \"name\": \"archived\",\n \"required\": false,\n \"schema\": {\n \"default\": false,\n \"example\": null,\n \"type\": \"boolean\"\n }\n }\n ],\n \"path\": \"/crm/v3/objects/contacts/batch/read\",\n \"request_body\": {\n \"$ref\": \"#/components/schemas/BatchReadInputSimplePublicObjectId\"\n },\n \"security\": [\n {\n \"oauth2\": [\n \"crm.objects.contacts.read\"\n ]\n }\n ],\n \"skill_name\": \"post_crm_v3_objects_contacts_batch_read_read\",\n \"source_index\": 1,\n \"source_openapi_url\": \"https://raw.githubusercontent.com/HubSpot/HubSpot-public-api-spec-collection/main/PublicApiSpecs/CRM/Contacts/Rollouts/424/v3/contacts.json\",\n \"source_title\": \"Contacts\",\n \"summary\": \"Retrieve a batch of contacts\",\n \"tags\": [\n \"Batch\"\n ]\n },\n \"post_crm_v3_objects_contacts_batch_update_update\": {\n \"base_url\": \"https://api.hubapi.com\",\n \"base_url_field\": \"OPENAPI_BASE_URL\",\n \"description\": \"Update a batch of contacts by ID (`contactId`) or unique property value (`idProperty`). Provided property values will be overwritten. Read-only and non-existent properties will result in an error. Properties values can be cleared by passing an empty string.\",\n \"destructive\": true,\n \"method\": \"POST\",\n \"operation_id\": \"post_crm_v3_objects_contacts_batch_update_update\",\n \"parameters\": [],\n \"path\": \"/crm/v3/objects/contacts/batch/update\",\n \"request_body\": {\n \"$ref\": \"#/components/schemas/BatchInputSimplePublicObjectBatchInput\"\n },\n \"security\": [\n {\n \"oauth2\": [\n \"crm.objects.contacts.write\"\n ]\n }\n ],\n \"skill_name\": \"post_crm_v3_objects_contacts_batch_update_update\",\n \"source_index\": 1,\n \"source_openapi_url\": \"https://raw.githubusercontent.com/HubSpot/HubSpot-public-api-spec-collection/main/PublicApiSpecs/CRM/Contacts/Rollouts/424/v3/contacts.json\",\n \"source_title\": \"Contacts\",\n \"summary\": \"Update a batch of contacts\",\n \"tags\": [\n \"Batch\"\n ]\n },\n \"post_crm_v3_objects_contacts_batch_upsert_upsert\": {\n \"base_url\": \"https://api.hubapi.com\",\n \"base_url_field\": \"OPENAPI_BASE_URL\",\n \"description\": \"Upsert a batch of contacts. The `inputs` array can contain a `properties` object to define property values for each record.\",\n \"destructive\": true,\n \"method\": \"POST\",\n \"operation_id\": \"post_crm_v3_objects_contacts_batch_upsert_upsert\",\n \"parameters\": [],\n \"path\": \"/crm/v3/objects/contacts/batch/upsert\",\n \"request_body\": {\n \"$ref\": \"#/components/schemas/BatchInputSimplePublicObjectBatchInputUpsert\"\n },\n \"security\": [\n {\n \"oauth2\": [\n \"crm.objects.contacts.write\"\n ]\n }\n ],\n \"skill_name\": \"post_crm_v3_objects_contacts_batch_upsert_upsert\",\n \"source_index\": 1,\n \"source_openapi_url\": \"https://raw.githubusercontent.com/HubSpot/HubSpot-public-api-spec-collection/main/PublicApiSpecs/CRM/Contacts/Rollouts/424/v3/contacts.json\",\n \"source_title\": \"Contacts\",\n \"summary\": \"Create or update a batch of contacts\",\n \"tags\": [\n \"Batch\"\n ]\n },\n \"post_crm_v3_objects_contacts_create\": {\n \"base_url\": \"https://api.hubapi.com\",\n \"base_url_field\": \"OPENAPI_BASE_URL\",\n \"description\": \"Create a single contact. Include a `properties` object to define [property values](https://developers.hubspot.com/docs/guides/api/crm/properties) for the contact, along with an `associations` array to define [associations](https://developers.hubspot.com/docs/guides/api/crm/associations/associations-v4) with other CRM records.\",\n \"destructive\": true,\n \"method\": \"POST\",\n \"operation_id\": \"post_crm_v3_objects_contacts_create\",\n \"parameters\": [],\n \"path\": \"/crm/v3/objects/contacts\",\n \"request_body\": {\n \"$ref\": \"#/components/schemas/SimplePublicObjectInputForCreate\"\n },\n \"security\": [\n {\n \"oauth2\": [\n \"crm.objects.contacts.write\"\n ]\n }\n ],\n \"skill_name\": \"post_crm_v3_objects_contacts_create\",\n \"source_index\": 1,\n \"source_openapi_url\": \"https://raw.githubusercontent.com/HubSpot/HubSpot-public-api-spec-collection/main/PublicApiSpecs/CRM/Contacts/Rollouts/424/v3/contacts.json\",\n \"source_title\": \"Contacts\",\n \"summary\": \"Create a contact\",\n \"tags\": [\n \"Basic\"\n ]\n },\n \"post_crm_v3_objects_contacts_gdpr_delete_purge\": {\n \"base_url\": \"https://api.hubapi.com\",\n \"base_url_field\": \"OPENAPI_BASE_URL\",\n \"description\": \"Permanently delete a contact and all associated content to follow GDPR. Use optional property `idProperty` set to `email` to identify contact by email address. If email address is not found, the email address will be added to a blocklist and prevent it from being used in the future. Learn more about [permanently deleting contacts](https://knowledge.hubspot.com/privacy-and-consent/how-do-i-perform-a-gdpr-delete-in-hubspot).\",\n \"destructive\": true,\n \"method\": \"POST\",\n \"operation_id\": \"post_crm_v3_objects_contacts_gdpr_delete_purge\",\n \"parameters\": [],\n \"path\": \"/crm/v3/objects/contacts/gdpr-delete\",\n \"request_body\": {\n \"$ref\": \"#/components/schemas/PublicGdprDeleteInput\"\n },\n \"security\": [\n {\n \"oauth2\": [\n \"crm.objects.contacts.write\"\n ]\n }\n ],\n \"skill_name\": \"post_crm_v3_objects_contacts_gdpr_delete_purge\",\n \"source_index\": 1,\n \"source_openapi_url\": \"https://raw.githubusercontent.com/HubSpot/HubSpot-public-api-spec-collection/main/PublicApiSpecs/CRM/Contacts/Rollouts/424/v3/contacts.json\",\n \"source_title\": \"Contacts\",\n \"summary\": \"Permanently delete a contact (GDPR-compliant)\",\n \"tags\": [\n \"Advanced\"\n ]\n },\n \"post_crm_v3_objects_contacts_merge_merge\": {\n \"base_url\": \"https://api.hubapi.com\",\n \"base_url_field\": \"OPENAPI_BASE_URL\",\n \"description\": \"Merge two contact records. Learn more about [merging records](https://knowledge.hubspot.com/records/merge-records). \",\n \"destructive\": true,\n \"method\": \"POST\",\n \"operation_id\": \"post_crm_v3_objects_contacts_merge_merge\",\n \"parameters\": [],\n \"path\": \"/crm/v3/objects/contacts/merge\",\n \"request_body\": {\n \"$ref\": \"#/components/schemas/PublicMergeInput\"\n },\n \"security\": [\n {\n \"oauth2\": [\n \"crm.objects.contacts.write\"\n ]\n }\n ],\n \"skill_name\": \"post_crm_v3_objects_contacts_merge_merge\",\n \"source_index\": 1,\n \"source_openapi_url\": \"https://raw.githubusercontent.com/HubSpot/HubSpot-public-api-spec-collection/main/PublicApiSpecs/CRM/Contacts/Rollouts/424/v3/contacts.json\",\n \"source_title\": \"Contacts\",\n \"summary\": \"Merge two contacts\",\n \"tags\": [\n \"Advanced\"\n ]\n },\n \"post_crm_v3_objects_contacts_search_do_search\": {\n \"base_url\": \"https://api.hubapi.com\",\n \"base_url_field\": \"OPENAPI_BASE_URL\",\n \"description\": \"Search for contacts by filtering on properties, searching through associations, and sorting results. Learn more about [CRM search](https://developers.hubspot.com/docs/guides/api/crm/search#make-a-search-request).\",\n \"destructive\": true,\n \"method\": \"POST\",\n \"operation_id\": \"post_crm_v3_objects_contacts_search_do_search\",\n \"parameters\": [],\n \"path\": \"/crm/v3/objects/contacts/search\",\n \"request_body\": {\n \"$ref\": \"#/components/schemas/PublicObjectSearchRequest\"\n },\n \"security\": [\n {\n \"oauth2\": [\n \"crm.objects.contacts.read\"\n ]\n }\n ],\n \"skill_name\": \"post_crm_v3_objects_contacts_search_do_search\",\n \"source_index\": 1,\n \"source_openapi_url\": \"https://raw.githubusercontent.com/HubSpot/HubSpot-public-api-spec-collection/main/PublicApiSpecs/CRM/Contacts/Rollouts/424/v3/contacts.json\",\n \"source_title\": \"Contacts\",\n \"summary\": \"Search for contacts\",\n \"tags\": [\n \"Search\"\n ]\n }\n}")
OPERATION_GROUPS = json.loads("[]")
ROOT_SECURITY = json.loads("[]")
SECURITY_SCHEMES = json.loads("{\n \"developer_hapikey\": {\n \"in\": \"query\",\n \"name\": \"hapikey\",\n \"type\": \"apiKey\"\n },\n \"oauth2\": {\n \"flows\": {\n \"authorizationCode\": {\n \"authorizationUrl\": \"https://app.hubspot.com/oauth/authorize\",\n \"scopes\": {\n \"crm.objects.commercepayments.read\": \"\",\n \"crm.objects.contacts.read\": \"\",\n \"crm.objects.contacts.write\": \"\",\n \"oauth\": \"\"\n },\n \"tokenUrl\": \"https://api.hubapi.com/oauth/v1/token\"\n }\n },\n \"type\": \"oauth2\"\n },\n \"private_apps\": {\n \"in\": \"header\",\n \"name\": \"private-app\",\n \"type\": \"apiKey\"\n },\n \"private_apps_legacy\": {\n \"in\": \"header\",\n \"name\": \"private-app-legacy\",\n \"type\": \"apiKey\"\n }\n}")
SECURITY_FIELDS = json.loads("{\n \"developer_hapikey\": {\n \"field\": \"HAPIKEY\",\n \"kind\": \"apiKey\",\n \"location\": \"query\",\n \"name\": \"hapikey\"\n },\n \"oauth2\": {\n \"field\": \"OAUTH2\",\n \"kind\": \"oauth2\",\n \"scheme\": \"Bearer\"\n },\n \"private_apps\": {\n \"field\": \"PRIVATE_APP\",\n \"kind\": \"apiKey\",\n \"location\": \"header\",\n \"name\": \"private-app\"\n },\n \"private_apps_legacy\": {\n \"field\": \"PRIVATE_APP_LEGACY\",\n \"kind\": \"apiKey\",\n \"location\": \"header\",\n \"name\": \"private-app-legacy\"\n }\n}")
PATH_PARAMETER_RE = re.compile(r"{([^}/]+)}")
SOURCE_ROOT = Path(globals().get("__file__", "agent.py")).resolve().parent
SOURCE_SKILLS_DIR = SOURCE_ROOT / "skills"
RUNTIME_SKILLS_DIR = ".deepagents/openapi-skills/"
class OperationInput(BaseModel):
parameters: dict[str, Any] = Field(
default_factory=dict,
description="Path, query, header, and cookie parameters keyed by OpenAPI parameter name.",
)
body: Any | None = Field(default=None, description="JSON request body, when the operation accepts one.")
class HubspotContactsOpenapiAgent(A2AAgent):
name = "hubspot-contacts-openapi-agent"
description = "CRM object API for creating, reading, searching, and batching HubSpot contacts."
version = "v3"
consumer_setup = ConsumerSetup.from_fields(
ConsumerSetupField.config("OPENAPI_BASE_URL", label="API base URL", description="Override the default API server (https://api.hubapi.com).", required=False, input_type="url"),
ConsumerSetupField.secret("HAPIKEY", label="developer_hapikey API key", description="Sent as query parameter 'hapikey'.", required=False),
ConsumerSetupField.secret("OAUTH2", label="oauth2 access token", description="OAuth/OIDC access token.", required=True),
ConsumerSetupField.secret("PRIVATE_APP", label="private_apps API key", description="Sent as header parameter 'private-app'.", required=False),
ConsumerSetupField.secret("PRIVATE_APP_LEGACY", label="private_apps_legacy API key", description="Sent as header parameter 'private-app-legacy'.", required=False),
)
llm_provisioning = LLMProvisioning.PLATFORM
pricing = Pricing(
price_per_call_usd=0.0,
caller_pays_llm=True,
notes="Uses the caller's saved LLM credential through ctx.llm.",
)
resources = Resources(cpu="200m", memory="512Mi")
egress = EgressPolicy(
allow_hosts=('api.hubapi.com',),
deny_internet_by_default=True,
)
tools_used = ("openapi", "deepagents")
capabilities = {
"openapi_auto_agent": {
"operation_count": len(OPERATIONS),
"default_base_url": DEFAULT_BASE_URL,
"source_openapi_url": 'https://raw.githubusercontent.com/HubSpot/HubSpot-public-api-spec-collection/main/PublicApiSpecs/CRM/Contacts/Rollouts/424/v3/contacts.json',
"source_openapi_urls": ['https://raw.githubusercontent.com/HubSpot/HubSpot-public-api-spec-collection/main/PublicApiSpecs/CRM/Contacts/Rollouts/424/v3/contacts.json'],
"server_urls": ['https://api.hubapi.com'],
"regenerable": True,
"security_schemes": list(SECURITY_SCHEMES),
}
}
@skill(
name="auto",
description="Use the OpenAPI service to complete a natural-language goal.",
tags=("openapi", "auto"),
timeout_seconds=900,
)
async def auto(self, ctx: RunContext, goal: str) -> dict[str, Any]:
creds = ctx.llm
if not creds.api_key:
return {
"error": "llm_credentials_missing",
"final": (
"LLM key required. Add an LLM credential in Settings > "
"LLM credentials before running this agent."
),
"messages": [],
}
graph = create_a2a_deep_agent(
ctx,
creds=creds,
tools=self._operation_tools(ctx),
system_prompt=self._system_prompt(),
)
result = await graph.ainvoke({"messages": [{"role": "user", "content": goal}]})
messages = result.get("messages", []) if isinstance(result, dict) else []
final = _message_text(messages[-1]) if messages else result
return {
"final": final,
"messages": [_message_to_dict(message) for message in messages[-8:]],
}
@skill(
name="get_crm_v3_objects_contacts_get_page",
description="GET /crm/v3/objects/contacts - Retrieve contacts",
tags=('Basic',),
timeout_seconds=60,
)
async def get_crm_v3_objects_contacts_get_page(
self,
ctx: RunContext,
parameters: dict[str, Any] | None = None,
body: Any | None = None,
) -> dict[str, Any]:
return await self._request(
ctx,
"get_crm_v3_objects_contacts_get_page",
parameters=parameters or {},
body=body,
)
@skill(
name="post_crm_v3_objects_contacts_create",
description="POST /crm/v3/objects/contacts - Create a contact",
tags=('Basic',),
timeout_seconds=120,
)
async def post_crm_v3_objects_contacts_create(
self,
ctx: RunContext,
parameters: dict[str, Any] | None = None,
body: Any | None = None,
) -> dict[str, Any]:
return await self._request(
ctx,
"post_crm_v3_objects_contacts_create",
parameters=parameters or {},
body=body,
)
@skill(
name="post_crm_v3_objects_contacts_batch_archive_archive",
description="POST /crm/v3/objects/contacts/batch/archive - Archive a batch of contacts",
tags=('Batch',),
timeout_seconds=120,
)
async def post_crm_v3_objects_contacts_batch_archive_archive(
self,
ctx: RunContext,
parameters: dict[str, Any] | None = None,
body: Any | None = None,
) -> dict[str, Any]:
return await self._request(
ctx,
"post_crm_v3_objects_contacts_batch_archive_archive",
parameters=parameters or {},
body=body,
)
@skill(
name="post_crm_v3_objects_contacts_batch_create_create",
description="POST /crm/v3/objects/contacts/batch/create - Create a batch of contacts",
tags=('Batch',),
timeout_seconds=120,
)
async def post_crm_v3_objects_contacts_batch_create_create(
self,
ctx: RunContext,
parameters: dict[str, Any] | None = None,
body: Any | None = None,
) -> dict[str, Any]:
return await self._request(
ctx,
"post_crm_v3_objects_contacts_batch_create_create",
parameters=parameters or {},
body=body,
)
@skill(
name="post_crm_v3_objects_contacts_batch_read_read",
description="POST /crm/v3/objects/contacts/batch/read - Retrieve a batch of contacts",
tags=('Batch',),
timeout_seconds=120,
)
async def post_crm_v3_objects_contacts_batch_read_read(
self,
ctx: RunContext,
parameters: dict[str, Any] | None = None,
body: Any | None = None,
) -> dict[str, Any]:
return await self._request(
ctx,
"post_crm_v3_objects_contacts_batch_read_read",
parameters=parameters or {},
body=body,
)
@skill(
name="post_crm_v3_objects_contacts_batch_update_update",
description="POST /crm/v3/objects/contacts/batch/update - Update a batch of contacts",
tags=('Batch',),
timeout_seconds=120,
)
async def post_crm_v3_objects_contacts_batch_update_update(
self,
ctx: RunContext,
parameters: dict[str, Any] | None = None,
body: Any | None = None,
) -> dict[str, Any]:
return await self._request(
ctx,
"post_crm_v3_objects_contacts_batch_update_update",
parameters=parameters or {},
body=body,
)
@skill(
name="post_crm_v3_objects_contacts_batch_upsert_upsert",
description="POST /crm/v3/objects/contacts/batch/upsert - Create or update a batch of contacts",
tags=('Batch',),
timeout_seconds=120,
)
async def post_crm_v3_objects_contacts_batch_upsert_upsert(
self,
ctx: RunContext,
parameters: dict[str, Any] | None = None,
body: Any | None = None,
) -> dict[str, Any]:
return await self._request(
ctx,
"post_crm_v3_objects_contacts_batch_upsert_upsert",
parameters=parameters or {},
body=body,
)
@skill(
name="post_crm_v3_objects_contacts_gdpr_delete_purge",
description="POST /crm/v3/objects/contacts/gdpr-delete - Permanently delete a contact (GDPR-compliant)",
tags=('Advanced',),
timeout_seconds=120,
)
async def post_crm_v3_objects_contacts_gdpr_delete_purge(
self,
ctx: RunContext,
parameters: dict[str, Any] | None = None,
body: Any | None = None,
) -> dict[str, Any]:
return await self._request(
ctx,
"post_crm_v3_objects_contacts_gdpr_delete_purge",
parameters=parameters or {},
body=body,
)
@skill(
name="post_crm_v3_objects_contacts_merge_merge",
description="POST /crm/v3/objects/contacts/merge - Merge two contacts",
tags=('Advanced',),
timeout_seconds=120,
)
async def post_crm_v3_objects_contacts_merge_merge(
self,
ctx: RunContext,
parameters: dict[str, Any] | None = None,
body: Any | None = None,
) -> dict[str, Any]:
return await self._request(
ctx,
"post_crm_v3_objects_contacts_merge_merge",
parameters=parameters or {},
body=body,
)
@skill(
name="post_crm_v3_objects_contacts_search_do_search",
description="POST /crm/v3/objects/contacts/search - Search for contacts",
tags=('Search',),
timeout_seconds=120,
)
async def post_crm_v3_objects_contacts_search_do_search(
self,
ctx: RunContext,
parameters: dict[str, Any] | None = None,
body: Any | None = None,
) -> dict[str, Any]:
return await self._request(
ctx,
"post_crm_v3_objects_contacts_search_do_search",
parameters=parameters or {},
body=body,
)
@skill(
name="get_crm_v3_objects_contacts_contact_id_get_by_id",
description="GET /crm/v3/objects/contacts/{contactId} - Retrieve a contact",
tags=('Basic',),
timeout_seconds=60,
)
async def get_crm_v3_objects_contacts_contact_id_get_by_id(
self,
ctx: RunContext,
parameters: dict[str, Any] | None = None,
body: Any | None = None,
) -> dict[str, Any]:
return await self._request(
ctx,
"get_crm_v3_objects_contacts_contact_id_get_by_id",
parameters=parameters or {},
body=body,
)
@skill(
name="delete_crm_v3_objects_contacts_contact_id_archive",
description="DELETE /crm/v3/objects/contacts/{contactId} - Archive a contact",
tags=('Basic',),
timeout_seconds=120,
)
async def delete_crm_v3_objects_contacts_contact_id_archive(
self,
ctx: RunContext,
parameters: dict[str, Any] | None = None,
body: Any | None = None,
) -> dict[str, Any]:
return await self._request(
ctx,
"delete_crm_v3_objects_contacts_contact_id_archive",
parameters=parameters or {},
body=body,
)
@skill(
name="patch_crm_v3_objects_contacts_contact_id_update",
description="PATCH /crm/v3/objects/contacts/{contactId} - Update a contact",
tags=('Basic',),
timeout_seconds=120,
)
async def patch_crm_v3_objects_contacts_contact_id_update(
self,
ctx: RunContext,
parameters: dict[str, Any] | None = None,
body: Any | None = None,
) -> dict[str, Any]:
return await self._request(
ctx,
"patch_crm_v3_objects_contacts_contact_id_update",
parameters=parameters or {},
body=body,
)
def _operation_subagents(self, ctx: RunContext, skills_root: str) -> list[dict[str, Any]]:
subagents: list[dict[str, Any]] = []
for group in OPERATION_GROUPS:
skill_path = (
f"{skills_root}{group['skill_dir']}/"
if skills_root
else ""
)
subagent = {
"name": group["subagent_name"],
"description": group["description"],
"system_prompt": self._subagent_prompt(group),
"tools": self._operation_tools(ctx, set(group["operation_ids"])),
}
if skill_path:
subagent["skills"] = [skill_path]
subagents.append(subagent)
return subagents
def _operation_tools(
self,
ctx: RunContext,
operation_ids: set[str] | None = None,
) -> list[StructuredTool]:
tools: list[StructuredTool] = []
for operation_id, operation in OPERATIONS.items():
if operation_ids is not None and operation_id not in operation_ids:
continue
async def call(
parameters: dict[str, Any] | None = None,
body: Any | None = None,
*,
_operation_id: str = operation_id,
) -> dict[str, Any]:
return await self._request(
ctx,
_operation_id,
parameters=parameters or {},
body=body,
)
tools.append(
StructuredTool.from_function(
coroutine=call,
name=operation["skill_name"],
description=self._tool_description(operation),
args_schema=OperationInput,
)
)
return tools
def _system_prompt(self) -> str:
if OPERATION_GROUPS:
lines = [
"You coordinate route-specific OpenAPI subagents.",
"Delegate API work to the matching subagent with the task tool.",
"Do not invent API responses; ask subagents to call their tools for real results.",
"If a subagent reports missing consumer setup, tell the user which setup field is required.",
"If a subagent reports that the generated OpenAPI agent may be stale, ask whether the user wants to refresh it from the latest OpenAPI spec.",
"",
"Available route groups:",
]
for group in OPERATION_GROUPS:
lines.append(
f"- {group['subagent_name']}: {group['description']}"
)
return "\n".join(lines)
lines = [
"You operate an API through generated OpenAPI tools.",
"Call tools to get real results. Do not invent API responses.",
"For write, update, or delete operations, explain the intended action before calling the tool.",
"If an operation reports missing consumer setup, tell the user which setup field is required.",
"If an operation returns 404, 405, 410, or a schema/validation error that suggests the live API no longer matches these tools, tell the user this generated agent may need to be refreshed from the latest OpenAPI spec and ask whether they want to refresh it.",
"",
"Available operations:",
]
for operation in OPERATIONS.values():
marker = "WRITE" if operation.get("destructive") else "READ"
lines.append(
f"- {operation['skill_name']}: {marker} {operation['method']} {operation['path']}{operation['summary']}"
)
return "\n".join(lines)
def _subagent_prompt(self, group: dict[str, Any]) -> str:
lines = [
"You operate one route group from a generated OpenAPI API.",
"Use the available operation tools to make real API calls. Do not invent API responses.",
"Read the route group's SKILL.md when it applies; it lists every route you cover.",
"For write, update, or delete operations, explain the intended action before calling the tool.",
"If an operation reports missing consumer setup, return the exact setup field name.",
"If an operation returns 404, 405, 410, or a schema/validation error that suggests the live API no longer matches these tools, say the generated agent may need to be refreshed from the latest OpenAPI spec.",
"",
f"Route group: {group['label']}",
f"Common prefix: {group.get('common_prefix') or '/'}",
"Available operation tools:",
]
for operation_id in group["operation_ids"]:
operation = OPERATIONS[operation_id]
marker = "WRITE" if operation.get("destructive") else "READ"
lines.append(
f"- {operation['skill_name']}: {marker} {operation['method']} {operation['path']} - {operation['summary']}"
)
return "\n".join(lines)
def _tool_description(self, operation: dict[str, Any]) -> str:
parts = [
f"{operation['method']} {operation['path']}",
str(operation.get("description") or operation.get("summary") or ""),
]
if operation.get("parameters"):
parts.append(
"Parameters: "
+ ", ".join(
f"{p['name']} in {p['in']}{' required' if p.get('required') else ''}"
for p in operation["parameters"]
)
)
if operation.get("request_body") is not None:
parts.append("Accepts a JSON request body.")
return "\n".join(part for part in parts if part)
async def _request(
self,
ctx: RunContext,
operation_id: str,
*,
parameters: dict[str, Any],
body: Any | None,
) -> dict[str, Any]:
operation = OPERATIONS[operation_id]
default_base_url = str(operation.get("base_url") or DEFAULT_BASE_URL).rstrip("/")
base_url_field = str(operation.get("base_url_field") or "OPENAPI_BASE_URL")
base_url = str(ctx.consumer_config(base_url_field, default_base_url) or default_base_url).rstrip("/")
url, query, headers = self._request_parts(ctx, operation, parameters)
request_kwargs: dict[str, Any] = {
"params": query,
"headers": headers,
}
if body is not None:
if isinstance(body, (bytes, bytearray)):
body = body.decode("utf-8", errors="replace")
if isinstance(body, str):
stripped = body.strip()
if stripped and stripped[0] in "{[":
try:
body = json.loads(stripped)
except ValueError:
pass
request_kwargs["json"] = body
async with httpx.AsyncClient(timeout=60.0, follow_redirects=True) as client:
response = await client.request(
operation["method"],
f"{base_url}{url}",
**request_kwargs,
)
content_type = response.headers.get("content-type", "")
try:
payload: Any = response.json() if "json" in content_type else response.text
except ValueError:
payload = response.text
if response.status_code >= 400:
out = {
"ok": False,
"status_code": response.status_code,
"operation_id": operation_id,
"error": payload,
}
if response.status_code in {404, 405, 410, 422}:
out["stale_openapi_hint"] = (
"This generated OpenAPI agent may be stale. Ask the user "
"whether they want to refresh it from the latest OpenAPI spec."
)
return out
return {
"ok": True,
"status_code": response.status_code,
"operation_id": operation_id,
"result": payload,
}
def _request_parts(
self,
ctx: RunContext,
operation: dict[str, Any],
parameters: dict[str, Any],
) -> tuple[str, dict[str, Any], dict[str, str]]:
path = operation["path"]
query: dict[str, Any] = {}
headers: dict[str, str] = {}
cookies: dict[str, Any] = {}
for param in operation.get("parameters") or []:
name = param["name"]
if name not in parameters:
if param.get("required"):
raise ValueError(f"missing required parameter {name!r}")
continue
value = parameters[name]
location = param["in"]
if location == "path":
path = path.replace("{" + name + "}", str(value))
elif location == "query":
query[name] = value
elif location == "header":
headers[name] = str(value)
elif location == "cookie":
cookies[name] = value
unresolved = PATH_PARAMETER_RE.findall(path)
if unresolved:
missing = ", ".join(sorted(set(unresolved)))
raise ValueError("missing required path parameter(s): " + missing)
if cookies:
headers["cookie"] = "; ".join(f"{key}={value}" for key, value in cookies.items())
auth_headers, auth_query = self._auth_for_operation(ctx, operation)
headers.update(auth_headers)
query.update(auth_query)
return path, query, headers
def _auth_for_operation(self, ctx: RunContext, operation: dict[str, Any]) -> tuple[dict[str, str], dict[str, Any]]:
requirements = operation.get("security")
if requirements is None:
requirements = ROOT_SECURITY
if requirements == []:
return {}, {}
missing: list[str] = []
for requirement in requirements or []:
if not isinstance(requirement, dict) or not requirement:
return {}, {}
headers: dict[str, str] = {}
query: dict[str, Any] = {}
ok = True
for scheme_name in requirement:
mapping = SECURITY_FIELDS.get(scheme_name) or {}
secret_name = mapping.get("field")
value = _consumer_secret_optional(ctx, secret_name) if secret_name else ""
if not value:
ok = False
if secret_name:
missing.append(secret_name)
continue
kind = mapping.get("kind")
if kind == "apiKey":
prefix = mapping.get("prefix") or ""
sent_value = f"{prefix}{value}"
if mapping.get("location") == "query":
query[mapping.get("name") or scheme_name] = sent_value
else:
headers[mapping.get("name") or scheme_name] = sent_value
elif kind in {"http", "oauth2"}:
scheme = mapping.get("scheme") or "Bearer"
if scheme.lower() == "basic":
token = base64.b64encode(value.encode("utf-8")).decode("ascii")
headers["authorization"] = f"Basic {token}"
else:
headers["authorization"] = f"{scheme} {value}"
else:
ok = False
if ok:
return headers, query
if missing:
raise ConsumerSetupMissing(
"operation requires consumer setup secret(s): "
+ ", ".join(sorted(set(missing)))
)
return {}, {}
def _consumer_secret_optional(ctx: RunContext, name: str | None) -> str:
if not name:
return ""
try:
return ctx.consumer_secret(name)
except ConsumerSetupMissing:
return ""
def _runtime_skills_root(ctx: RunContext) -> str:
workspace = getattr(ctx, "_workspace", None)
prefixes = tuple(getattr(workspace, "write_prefixes", ()) or ())
if not prefixes:
outputs_prefix = getattr(workspace, "outputs_prefix", None)
prefixes = (outputs_prefix or "outputs/",)
prefix = str(prefixes[0]).strip("/")
return f"/{prefix}/{RUNTIME_SKILLS_DIR}" if prefix else f"/{RUNTIME_SKILLS_DIR}"
def _seed_runtime_skills(backend: Any, ctx: RunContext) -> str:
if not SOURCE_SKILLS_DIR.exists():
return ""
runtime_root = _runtime_skills_root(ctx)
uploads: list[tuple[str, bytes]] = []
for path in SOURCE_SKILLS_DIR.rglob("*"):
if not path.is_file():
continue
rel = path.relative_to(SOURCE_SKILLS_DIR).as_posix()
uploads.append((runtime_root + rel, path.read_bytes()))
if not uploads:
return ""
backend.upload_files(uploads)
return runtime_root
def _message_text(message: Any) -> Any:
content = getattr(message, "content", message)
return content
def _message_to_dict(message: Any) -> dict[str, Any]:
if isinstance(message, BaseMessage):
return message.model_dump(mode="json")
if hasattr(message, "model_dump"):
return message.model_dump(mode="json")
if isinstance(message, dict):
return message
return {"content": str(message)}
agent = HubspotContactsOpenapiAgent()