跳到正文
DEV Community· Anand Prakash Srivastava·· 2 小時前AI 評分48

我開發的首個 cognee 連接器:將 Basecamp 同步至 AI 智能體記憶

My first cognee connector: syncing Basecamp into an AI agent's memory

AI 導讀

作者為 cognee 開發 Basecamp 連接器,將訊息、待辦事項、文件和評論同步至這個開源 AI 智能體記憶層,讓用戶可直接查詢專案內容。連接器利用 ETag 和 updated_at 遊標減少重複讀取,並每 10 次同步進行一次完整掃描,以識別已清空垃圾桶、無法從增量同步中偵測的項目。

正文

Basecamp is where a lot of small teams keep the boring but important stuff. The message that says when something launches. The to-do list of what is left. The comment under it saying "this needs a security review first". It is all there, just spread across a project in a way that is easy to lose track of.

Basecamp was the first connector I built for Mergetober. It syncs messages, to-dos, documents and comments into cognee, an open source memory layer for AI agents, so you can ask questions about a project instead of clicking through it. It is PR #352 in cognee-community, for issue #4707.

How I use it

I set up a small product launch across three tools to try my connectors together: discussion in Twist, tasks in Basecamp, the customer email in Mailchimp. (The Twist and Mailchimp connectors have their own posts.) Basecamp holds the "what is left to do" side.

My project has a "Q4 launch plan" message, a "Launch tasks" to-do list with "Write release notes" and "Fix login bug", and a couple of comments. Each item becomes one document. These are from my live sync:

# Q4 launch plan

Basecamp Message
Project: My Project
Author: Anand
Created: 2026-10-08

We will launch the mobile app on Nov 22. The payments team owns checkout.
# Comment on message "Q4 launch plan"

Basecamp Comment
Project: My Project
Author: Anand
Created: 2026-10-08
On message: "Q4 launch plan"

Checkout needs a security review first.
# Write release notes

Basecamp To-do
Project: My Project
Author: Anand
Created: 2026-10-08
To-do list: Launch tasks
Completed: yes
Due: 2026-10-09

Include the Q4 changes.

Two small things there took me a while to get right. A comment on its own ("Checkout needs a security review first.") means nothing, so every comment carries its parent's title. And a to-do carries its list, whether it is done, and its due date, because "which to-dos are done?" is one of the first things you ask.

The code to sync and ask:

import cognee
from cognee_community_connector_basecamp import basecamp_source

await cognee.remember(
    basecamp_source(
        "1234567",  # your account id
        access_token=token,
        user_agent="My Sync (me@example.com)",  # Basecamp requires one
    ),
    dataset_name="basecamp",
    write_disposition="merge",
    max_rows_per_table=0,
)
print(await cognee.recall("Which to-dos in Launch tasks are completed?", datasets=["basecamp"]))

On my account, with Gemini as the LLM and local embeddings, I got:

Question Answer from cognee
When is the mobile app launching? "The mobile app will launch on Nov 22."
Which to-dos in Launch tasks are completed? "'Write release notes' and 'Fix login bug' are completed."
What did people say about checkout? "Anand said that checkout needs a security review first and that the payments team owns it."

The first answer says Nov 22 because I had just edited the message from "Nov 15" to "Nov 22" and synced again. The old date was gone from memory, which was exactly the test.

How it works

One endpoint for everything

Basecamp's API is organised by project and tool. You can walk projects, then message boards, then messages, then comments, and so on. That is a lot of requests. But there is one endpoint that lists everything of a given type across the account:

GET /projects/recordings.json?type=Message&status=active&sort=updated_at&direction=desc

It takes type (Message, Todo, Document, Comment), status (active, archived, trashed), an optional bucket to limit it to some projects, and sorts newest first. The whole connector reads from this one endpoint. Paging follows only the Link: rel="next" header, never page numbers I make up myself.

Two ways to avoid doing work

ETags. The first page of each listing comes back with an ETag. Next run, I send it back as If-None-Match. If nothing changed, Basecamp answers 304 Not Modified with no body. Since any change bumps an item's updated_at and moves it to the top of a newest-first list, an unchanged first page really does mean nothing changed for that type.

An updated_at cursor. If something did change, I read from the top and stop as soon as I reach items older than the last run's newest timestamp (minus a five minute overlap, so a clock difference cannot make me miss something):

for rec in client.list_recordings(record_type, status=status, project_ids=config.project_ids, etag=etag):
    updated = _parse_ts(rec.get("updated_at"))
    if stop_before and updated and updated < stop_before:
        break
    ...
    yield row

Both live in dlt's resource state, which is only saved when the load succeeds. A run that fails halfway leaves the state where it was, and the next run starts from the same point.

Deletes are where it gets interesting

There are three ways an item can leave a project, and they need three different answers:

Trashed is easy. The item moves to status=trashed and its updated_at moves, so the next incremental run finds it in the trashed listing and yields a _deleted row. dlt removes the row and cognee forgets it.

Archived is not a delete. I checked live: the item moves to status=archived and its comments follow it. Nothing was lost, so archived items stay in memory, marked "Status: archived".

Purged is the hard one. When the trash is emptied, either by hand or automatically after 25 days, the item vanishes from every listing and a GET returns 404. There is nothing left to see in an incremental run. So every 10th run (or with full_sync=True) the connector does a full sweep: it lists everything that is active or archived, and anything it synced before that is not in that list was purged.

These are my live runs on the account:

Step Change in Basecamp Result in cognee
0 Baseline Full sync, 5 documents
1 Edited a message ("Nov 15" to "Nov 22") Only the changed item re-read; new text stored, old text gone
2 Completed a to-do Stored as "Completed: yes", still in memory
3 Created a to-do Added (6 documents)
4 Trashed that to-do 1 deletion, the other 5 unchanged

The bug I shipped in the first version

The sweep had a problem that I only saw after the reviewer on my Twist PR found the same pattern there. My sweep forgot every synced item that it did not see. Now think about what a sweep looks like when you pass a wrong project id, or the token loses access to a project, or the API returns something odd that parses as an empty list. It sees nothing. So it forgets everything.

I wrote a test for it on the old code: 12 synced items, a sweep with a wrong project filter, and 12 of 12 forgotten in one run. That is the whole dataset gone because of a typo.

Now:

  • If a sweep finds more than half of what it synced newly gone, and at least 10 items, it holds those deletes back and reports them as held_back. The very next run sweeps again (it does not wait for the 10th run), forgets the held items that are still gone, and keeps the ones that came back.
  • Items in the trash are never held. The trashed listing is direct evidence that the user deleted them.
  • A listing page that is not a JSON list, or a next link that points back at the same page, stops the run with an error instead of being read as "nothing here".
if not isinstance(items, list):
    # An odd 200 must never read as an empty listing: the sweep would
    # forget everything it holds.
    raise BasecampError(
        f"Basecamp: unexpected body for {_redact(str(url))}; nothing was synced or forgotten."
    )

The same review also changed how I handle Retry-After. My client used to sleep for whatever Basecamp asked, so Retry-After: 3600 meant a sync sleeping for an hour while cognee held its staging lock, and nan crashed time.sleep. Now any wait over 60 seconds stops the run as a rate limit so the next sync can retry, and a zero, negative or unreadable value uses exponential backoff. Every one of these fixes has a test that fails when I revert the fix.

Auth, and some small things the API taught me

Basecamp uses OAuth through 37signals Launchpad. examples/authorize.py does the one-time browser step and saves the tokens. Access tokens expire after two weeks, so the client can take a refresh token with the client id and secret and refreshes once on a 401. (I found that out the practical way: the token I tested with expired while I was writing this post.)

Other things I learned from the live API:

  • Basecamp's docs ask for a User-Agent on every request that names your app and a way to contact you, so the connector makes you pass one.
  • An expired trial or suspended account answers 404 with a Reason: Account Inactive header. I turned that into its own error, because "not found" is a confusing message for "your trial ended".
  • Completed to-dos are already in the default listing with completed: true, so no extra request is needed.
  • Message titles arrive with a trailing newline.
  • The token is never put in an error message. URLs in errors are redacted.

Tests

The tests run against an in-memory fake of the Basecamp API that copies what the live account did: ETags and 304s, trash, purge, archive, Link paging. One test runs through cognee itself (with the LLM mocked) and checks that both a trashed item and a purged item actually leave the knowledge graph.

Limits

  • Only active projects are read (the API default). Archiving a whole project removes its items on the next sweep.
  • Card tables, schedules, chat, uploads and check-in answers are not synced yet.
  • A purged item is only noticed on the next sweep, every 10th run by default.

Try it

The code is in packages/connector/basecamp/ in cognee-community. Register an app at launchpad.37signals.com, run examples/authorize.py, then examples/example.py. If your team lives in Basecamp, I would love to hear what you ask it first.

來源:DEV Community · dev.to