跳到正文
DEV Community· Oscar Kandir·· 3 小時前AI 評分54

Mergetober:為 cognee 建立支援關聯同步與刪除追蹤的 HubSpot CRM 連接器

Mergetober: a CRM memory that forgets, with cognee and HubSpot

AI 導讀

Oscar Kandir 為 cognee 建立 HubSpot 連接器,可同步聯絡人、公司、交易、工單和備註及其關係,並只處理變更、追蹤刪除或合併記錄。在示範中,刪除記有折扣的備註和審批人聯絡人後,下一次同步不再回答折扣與審批人資料,並把未修改聯絡人的公司更新為 Northwind Automation。

正文

A CRM holds a lot of small facts that only make sense together: this contact works for that company, this deal needs a discount, that discount needs someone's approval. An agent that can answer "what does Northwind want, and who has to sign off?" needs all of those links, and it needs to drop them again when the CRM changes.

For Mergetober I built a HubSpot connector for cognee. It syncs contacts, companies, deals, tickets and notes with the relationships between them, picks up only what changed, and forgets what is deleted or merged away.

Setting it up

The connector works with a HubSpot OAuth app (read scopes only) or a Service Key. examples/get_refresh_token.py gets the refresh token in one step: a browser consent, and the token is saved to a private file, never printed.

import cognee
from cognee_community_connector_hubspot import hubspot_source

await cognee.remember(
    hubspot_source(objects=["contacts", "companies", "deals", "tickets", "notes"]),
    dataset_name="hubspot_crm",
    primary_key="id",
    write_disposition="merge",
    max_rows_per_table=0,
)

Remember, then forget

On a free HubSpot account I set up a company with two contacts, a deal, a ticket and a note, next to HubSpot's own sample records:

The test records in HubSpot

Then I synced it and asked cognee (with OpenAI as the LLM):

What discount does Northwind want on the Warehouse Fleet Renewal, who must approve it, and which company does Priya Raman work for?

Before:

Before: cognee names the 12% discount, Arjun Mehta as approver, and Northwind Robotics

Northwind wants a 12% discount if it signs before November 15, 2026. Arjun Mehta must approve it because it exceeds 10%. Priya Raman works for Northwind Robotics.

Then I synced it and asked cognee (with OpenAI as the LLM):

What discount does Northwind want on the Warehouse Fleet Renewal, who must approve it, and which company does Priya Raman work for?

Before:

Before: cognee names the 12% discount, Arjun Mehta as approver, and Northwind Robotics

Northwind wants a 12% discount if it signs before November 15, 2026. Arjun Mehta must approve it because it exceeds 10%. Priya Raman works for Northwind Robotics.

Then, in HubSpot, I deleted the note that held the discount, deleted Arjun Mehta, and renamed the company to Northwind Automation. Deleted records go to a restorable list, kept 90 days, and that list is what the connector reads:

HubSpot's list of deleted records, with the note and Arjun Mehta

After the next sync, the same question:

After: discount and approver not specified, Priya at Northwind Automation

Discount: Not specified in the context. Approver: Not specified in the context. Priya Raman's company: Northwind Automation.

(The screenshots are from the repeat run, after restoring everything and deleting it again; the answers were the same both times.)

The discount lived only in the note, so it is forgotten. Arjun is gone from his own document and from the company's. And Priya, whose record I never touched, now works for the company's new name. I then restored everything from HubSpot's recycle bin, deleted it again, and both answers came back the same way.

Two decisions that changed from my plan

I didn't wrap dlt's verified HubSpot source. It's the obvious starting point, so I ran it on my account first. It's distributed through dlt init (copied into a project), not as a package a connector can depend on, and it had no cursor for CRM objects, no engagements (notes, calls, meetings), and no delete propagation: a contact I deleted was still there after a re-run. It also read every property. The connector uses the same dlt HTTP layer (RESTClient) with its own sync on top.

Documents, not tables. On the issue I proposed keeping CRM objects relational, so associations would become graph edges. Then I read how cognee's relational path works: it links rows only through database foreign keys, and dlt doesn't create those, so the association tables would have landed as unconnected rows. Writing the linked records into each document by name gives cognify the relationships directly.

How relationships become a graph

Each record is one document that names what it is linked to:

Contact: Priya Raman <priya@northwind.example.com>
  Job title: Head of Operations
  Companies: Northwind Robotics (northwind-robotics.example)
  Deals: Warehouse Fleet Renewal (48000, Appointment Scheduled)
  Tickets: Robot arm calibration fails after update (New)

cognify turns those names into entities, so one company mentioned in many documents becomes one node in the graph. That is also why a rename has to re-render every document that names the company, not just the company itself.

The tricky cases

Most of the work went into things that only show up on a real account:

  • Deleted records drop their associations. When a contact is deleted, HubSpot no longer lists her as linked to her company, so reading the company again shows nothing to update. The connector keeps its own index of which documents name which records, so the company's document drops her name too.
  • Merged records never show up as deleted. The merged-away id only appears in the survivor's hs_merged_object_ids, so the connector tombstones it from there.
  • HubSpot touches records on its own. After I deleted the note, HubSpot recalculated the deal's activity fields a few minutes later. Every touch re-rendered the contact and company linked to the deal, with identical content. Now only a changed label or a changed set of links refreshes linked documents.
  • Search stops at 10,000 results. A window with more changes restarts from the last timestamp seen, without skipping or repeating records.
  • Access tokens last 30 minutes. A long first sync would have failed halfway, and failed the same way on every retry. A 401 now mints a new token and retries once.

Deletes come from HubSpot's archived list, which keeps deleted records for 90 days. GDPR deletes, or records archived longer ago, need reconcile_deletes=True, which compares a full id listing.

Checking my own work

Two bugs I found while preparing the review fixes, before anyone else did:

  • An empty response could forget the whole CRM. A 200 with an empty body was read as {}. With reconcile_deletes=True, an empty id listing looked like "no records left", and every stored record would have been marked deleted. Any 200 without a JSON object now stops the run before anything is forgotten, tested through the real HTTP client.
  • My forget test proved too little. It stayed green even with the hard-delete marker switched off. It now checks that the deleted note's and contact's records are really gone from cognee, and that an untouched note keeps its id.

And from the live runs: the first read titled notes by id (Note 405507740407), since HubSpot notes have no subject. They now use the start of their text.

Privacy

Contacts have about 90 properties by default, including date of birth and gender. The connector reads an allowlist of properties, and the sensitive ones are never requested unless you ask for them with extra_properties.

What I learned

In a CRM, a delete is never just one record. Arjun's contact was one row, but his name was on the company's document too, and HubSpot stopped listing the link the moment he was deleted, so there was nothing left to tell the company's document to change. Relationships are where memory goes stale. Now, for every connector, I ask: when this record changes or disappears, which other documents mention it?

The setup steps are in the README in the pull request, and examples/example.py syncs your CRM and asks it a question.

來源:DEV Community · dev.to