Let people name accounts and use the names to tell accounts apart

Connections get an account_name (migration 0039) that the owner sets with
PATCH /api/connections/<id>; the account label stays the account's
identity, so signing in again still finds it. When someone has more than
one account of a service, its tools are listed as "Telegram · <account>"
and the model sees each account's actions under the account's name
(telegram_send_message_alerts_bot) with the account in the description,
instead of _1 and _2. Actions shared by different services are named
after the service. Tools a user renamed keep their name.
This commit is contained in:
arc53-machine committed 2026-09-29 10:41:54 +01:00
1 parent 607c2296e3
commit bdcb88866a
11 files changed
+371 -7

No files matched your search

+35
View File
@@ -417,6 +417,41 @@ class TestParameters:
assert chat_id["filled_by_llm"] is True
class TestRename:
@staticmethod
def _patch(app, cid, body, user="alice"):
from docsgpt.api.connector.connections import ConnectionDetail
return _call(app, ConnectionDetail, "patch", f"/api/connections/{cid}", user=user, body=body, args=[cid])
def test_owner_names_and_unnames_an_account(self, app, pg_conn):
cid = _connection(pg_conn, secrets={"credentials": {"token": "t"}})
with _db(pg_conn):
named = self._patch(app, cid, {"name": " Alerts bot "})
assert named.status_code == 200
connection = named.get_json()["connection"]
assert connection["account_name"] == "Alerts bot"
# The label stays the account's identity.
assert connection["account_label"] == "…abcd"
cleared = self._patch(app, cid, {"name": ""})
assert cleared.get_json()["connection"]["account_name"] is None
@pytest.mark.parametrize("body", [{"name": "x" * 81}, {"name": 5}, {}])
def test_rejects_bad_names(self, app, pg_conn, body):
cid = _connection(pg_conn, secrets={})
with _db(pg_conn):
assert self._patch(app, cid, body).status_code == 400
def test_another_user_cannot_rename(self, app, pg_conn):
cid = _connection(pg_conn, secrets={})
with _db(pg_conn):
assert self._patch(app, cid, {"name": "Mine now"}, user="mallory").status_code == 404
name = pg_conn.execute(
text("SELECT account_name FROM connector_sessions WHERE id = CAST(:i AS uuid)"), {"i": cid}
).scalar()
assert name is None
class TestDelete:
def test_delete_with_source_removal(self, app, pg_conn):
from docsgpt.api.connector.connections import ConnectionDetail
+49
View File
@@ -276,6 +276,55 @@ class TestTelegramDefaultChat:
assert call.kwargs == {"text": "hi", "chat_id": "-2002"}
class TestAccountsTellApartForTheModel:
@staticmethod
def _two_bots(pg_conn, names):
tools = {}
for index, name in enumerate(names):
cid = _connection(pg_conn)
pg_conn.execute(text(
"UPDATE connector_sessions SET account_label = :l, account_name = :n WHERE id = CAST(:i AS uuid)"
), {"l": f"…{index}abc", "n": name, "i": cid})
tools[f"t{index}"] = {**_telegram_tool(cid), "id": f"tool-{index}"}
return tools
def test_named_accounts_name_the_functions(self, pg_conn):
tools = self._two_bots(pg_conn, ["Alerts bot", "Ops: on-call!"])
with _service_db(pg_conn):
executor = _executor()
functions = {f["function"]["name"]: f["function"] for f in executor.prepare_tools_for_llm(tools)}
assert {"telegram_send_message_alerts_bot", "telegram_send_message_ops_on_call"} <= set(functions)
assert "Alerts bot" in functions["telegram_send_message_alerts_bot"]["description"]
assert executor._name_to_tool["telegram_send_message_ops_on_call"] == ("t1", "telegram_send_message")
def test_unnamed_accounts_use_their_labels(self, pg_conn):
tools = self._two_bots(pg_conn, [None, None])
with _service_db(pg_conn):
names = {f["function"]["name"] for f in _executor().prepare_tools_for_llm(tools)}
assert {"telegram_send_message_0abc", "telegram_send_message_1abc"} <= names
def test_different_services_are_named_after_the_service(self, pg_conn):
tools = {}
for index, (host, name) in enumerate((("a.example.com", "Wiki"), ("b.example.com", "Tracker"))):
cid = _connection(pg_conn, provider=f"mcp:https://{host}", auth_kind="mcp_oauth",
server_url=f"https://{host}", secrets={"tokens": {"access_token": "x"}})
pg_conn.execute(text("UPDATE connector_sessions SET connector_key = 'custom_mcp', display_name = :n "
"WHERE id = CAST(:i AS uuid)"), {"n": name, "i": cid})
tools[f"t{index}"] = {**_tool(cid, name="mcp_tool", tool_id=f"tool-{index}"),
"actions": [{"name": "search", "description": "Search", "active": True}]}
with _service_db(pg_conn):
functions = {f["function"]["name"]: f["function"] for f in _executor().prepare_tools_for_llm(tools)}
assert set(functions) == {"search_wiki", "search_tracker"}
assert functions["search_wiki"]["description"].startswith("Search (Wiki account:")
def test_names_stay_within_provider_limits(self, pg_conn):
tools = self._two_bots(pg_conn, ["x" * 80, "x" * 80])
with _service_db(pg_conn):
names = [f["function"]["name"] for f in _executor().prepare_tools_for_llm(tools)]
assert len(names) == len(set(names))
assert all(len(n) <= 64 and n.replace("_", "").replace("-", "").isalnum() for n in names)
class TestScheduledSync:
def test_connector_sources_with_a_connection_are_dispatched(self, pg_conn):
from docsgpt import worker
+49
View File
@@ -245,3 +245,52 @@ class TestAvailableTools:
assert tools["telegram"]["displayName"] == "Telegram"
assert tools["ntfy"]["displayName"] == "ntfy"
assert tools["postgres"]["displayName"] == "PostgreSQL"
def _telegram_connection(conn, user, label, name=None):
row = conn.execute(
text(
"INSERT INTO connector_sessions (user_id, provider, connector_key, auth_kind, status, account_label, "
"account_name, encrypted_credentials) VALUES (:u, 'telegram', 'telegram', 'api_key', 'connected', "
":l, :n, :e) RETURNING *"
),
{"u": user, "l": label, "n": name, "e": encrypt_json({"credentials": {"token": label}}, user)},
).one()
return dict(row._mapping)
class TestAccountNamesInToolNames:
def _listed(self, app, pg_conn, user="alice"):
from docsgpt.api.user.tools.routes import GetTools
with _db(pg_conn), app.test_request_context("/api/get_tools"):
from flask import request
request.decoded_token = {"sub": user}
tools = GetTools().get().get_json()["tools"]
return sorted(t["customName"] for t in tools if t.get("name") == "telegram")
def test_two_accounts_are_named_after_their_accounts(self, app, pg_conn):
for label, name in (("…aaaa", "Alerts bot"), ("…bbbb", None)):
service.ensure_connection_tools(pg_conn, "alice", _telegram_connection(pg_conn, "alice", label, name))
assert self._listed(app, pg_conn) == ["Telegram · Alerts bot", "Telegram · …bbbb"]
def test_one_account_keeps_the_plain_name(self, app, pg_conn):
connection = _telegram_connection(pg_conn, "alice", "…aaaa", "Alerts bot")
service.ensure_connection_tools(pg_conn, "alice", connection)
assert self._listed(app, pg_conn) == ["Telegram"]
def test_a_name_the_user_chose_is_kept(self, app, pg_conn):
for label in ("…aaaa", "…bbbb"):
service.ensure_connection_tools(pg_conn, "alice", _telegram_connection(pg_conn, "alice", label))
pg_conn.execute(text("UPDATE user_tools SET custom_name = 'Ops' WHERE name = 'telegram' "
"AND connection_id = (SELECT id FROM connector_sessions WHERE account_label = '…aaaa')"))
assert self._listed(app, pg_conn) == ["Ops", "Telegram · …bbbb"]
def test_the_drawer_names_the_tool_after_its_account_too(self, pg_conn):
named = _telegram_connection(pg_conn, "alice", "…aaaa", "Alerts bot")
for connection in (named, _telegram_connection(pg_conn, "alice", "…bbbb")):
service.ensure_connection_tools(pg_conn, "alice", connection)
detail = service.connection_detail(pg_conn, named)
assert detail["account_name"] == "Alerts bot"
assert detail["tools"][0]["display_name"] == "Telegram · Alerts bot"
+50
View File
@@ -0,0 +1,50 @@
"""Migration round-trip test for 0039_connection_account_name."""
from __future__ import annotations
import os
import subprocess
import sys
from pathlib import Path
import pytest
from sqlalchemy import text
pytestmark = pytest.mark.integration
_0039 = "0039_connection_account_name"
_0038 = "0038_connections"
def _run_alembic(url: str, *args: str) -> None:
ini = Path(__file__).resolve().parents[3] / "docsgpt" / "alembic.ini"
subprocess.check_call(
[sys.executable, "-m", "alembic", "-c", str(ini), *args],
timeout=120,
env={**os.environ, "POSTGRES_URI": url},
)
def _has_column(conn) -> bool:
return conn.execute(
text(
"SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' "
"AND table_name = 'connector_sessions' AND column_name = 'account_name'"
)
).fetchone() is not None
class TestMigration0039RoundTrip:
def test_head_has_account_name(self, pg_engine):
with pg_engine.connect() as conn:
assert _has_column(conn)
def test_downgrade_drops_then_upgrade_restores(self, pg_engine):
url = pg_engine.url.render_as_string(hide_password=False)
_run_alembic(url, "downgrade", _0038)
with pg_engine.connect() as conn:
assert not _has_column(conn)
_run_alembic(url, "upgrade", "head")
with pg_engine.connect() as conn:
assert _has_column(conn)