MemberJunction provider for Betty, the MJ-native organization-scoped assistant. Implements
BaseLLM against the Betty Public API v1, so an MJ prompt or agent can call Betty like any other
model.
@memberjunction/ai-betty-botThey are different services, not two versions of one. This package exists alongside the legacy
one — nothing in ai-betty-bot changes, no deployment has to migrate, and both can run in the same
instance.
BettyBotLLM (legacy) |
BettyLLM (this package) |
|
|---|---|---|
| Host | betty-api.tasio.co |
per-customer Betty deployment |
| Auth | key → POST /settings → JWT → bearer JWT |
key is the bearer |
| Endpoint | POST /response |
POST /messages |
| Request | { input } |
{ message, conversationId?, context? } |
| Response | { response, references[{link,title,type}] } |
{ conversationId, messageId, response, references[{title,url?,contentItemId?}], requestId } |
| History | first user message only | latest user turn + prior turns as context |
| Env var | AI_VENDOR_API_KEY__BETTYBOTLLM |
AI_VENDOR_API_KEY__BETTYLLM |
| Base URL | BETTY_BOT_BASE_URL (defaulted) |
BETTY_API_BASE_URL (required) |
Migrating a deployment is repointing one AIModelVendor.DriverClass from BettyBotLLM to
BettyLLM; rolling back is repointing it back.
BETTY_API_BASE_URL=https://<host>/betty/v1 # required — include the version segment
AI_VENDOR_API_KEY__BETTYLLM=<betty api key> # from Insights › External › API keys
BETTY_API_BASE_URL has no default on purpose. Betty is deployed per customer, so a default
would let a misconfigured instance answer from another tenant. Unset, the first call fails with a
message naming the variable.
The API key needs the betty:chat scope. Credentials can also come through MJ's credential system
(AICredentialBinding), which outranks the environment variable.
Runtime resolution is AIModelVendor.DriverClass → this class. You need:
AIModelTypeID = LLMDriverClass = 'BettyLLM' and an APINameDo not create MJ: AI Vendor Type Definitions rows — they are migration-seeded; reference them
by @lookup.
The constructor takes an optional second argument, forwarded as Betty's endUserId request field:
new BettyLLM(apiKey, 'izzy')
Betty records it against the conversation, so the caller gets its own attribution row and its own
rate-limit bucket instead of sharing an anonymous per-IP one. Without it, utilization reports read a
server as a single anonymous client — the problem PR #4113 fixes for BettyBotLLM with its userId
argument; this is the same idea under the field name Betty's own API documents.
Attribution only, and unverified by design: retrieval scope is pinned server-side from the credential, so it never widens what content a request can reach. Do not use it for authorization.
Omit it and the request body is byte-identical to one built without the parameter.
Note it is reachable only by constructing the provider directly. AIPromptRunner instantiates via
ClassFactory.CreateInstance(BaseLLM, driverClass, apiKey) and passes the key alone, and
ChatParams carries no user identity to fall back on — so a prompt run through the normal model
path is anonymous, exactly as the legacy provider is.
Betty threads conversations server-side through conversationId, but ChatParams carries no
correlation id and a fresh provider instance is constructed per execution, so there is nothing to
thread it through. MJ owns the history instead: the latest user message is the turn, and
everything before it goes in context.text — which the API documents as advisory and explicitly
non-authorizing, so it informs the answer without widening what the credential can reach.
This is the one behaviour deliberately not carried over from the legacy provider, which uses
messages.find(m => m.role === user) — .find returns the first match, so a three-turn exchange
sends the opening question and discards the follow-up.
References ride as extra choices, matching BettyBotLLM exactly:
choices[0] — the answerchoices[1] — references formatted for displaychoices[2] — raw JSON, finish_reason: 'references_json'Kept identical so anything already parsing the legacy provider's output keeps working across a
DriverClass repoint.
SupportsStreaming returns false. The API does support SSE via
Accept: text/event-stream, so this is a clean follow-up rather than a dead end.SummarizeText and ClassifyText are not implemented.