@dbx-tools/teams 0.3.44 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,234 @@
1
+ /**
2
+ * The Teams conversation turn: run a Mastra agent against an inbound Bot
3
+ * Framework activity and answer with Adaptive Card attachments.
4
+ *
5
+ * This is the piece that makes `POST /api/teams/activity` behave like a Teams
6
+ * bot rather than a chat API that happens to return JSON. A turn is:
7
+ *
8
+ * 1. read the user's text off the inbound `message` activity;
9
+ * 2. ANSWER it - a normal tool-using agent turn, with no mention of cards, so
10
+ * the agent queries Genie / calls its tools exactly as it would on a
11
+ * streaming chat endpoint;
12
+ * 3. FORMAT that answer into a {@link card.CardSpec} in a second pass, via
13
+ * Mastra's `structuredOutput` (prompt-injected, see
14
+ * {@link JSON_PROMPT_INJECTION});
15
+ * 4. compile the spec with the same deterministic builder the
16
+ * `create_teams_card` tool uses, and attach it to an outbound activity.
17
+ *
18
+ * The two passes are the important part. Asking for the answer AND the card
19
+ * shape in ONE request makes the model treat formatting as the task: it emits a
20
+ * card straight away and never calls its tools, so a question that should have
21
+ * queried a data source came back as "I don't have a real system connected -
22
+ * here is a template card with placeholders". Answering first, then formatting a
23
+ * REAL answer, makes this endpoint's content identical to the streaming
24
+ * endpoint's; only the presentation differs.
25
+ *
26
+ * Formatting is also why the turn does not simply rely on the agent calling
27
+ * `create_teams_card`: on this endpoint a card IS the response format, so it
28
+ * should be a property of the turn rather than a tool the model may forget.
29
+ * Agents keep the tool for the other direction - answering in prose on a normal
30
+ * chat endpoint and choosing to attach a card. When the agent DOES call it
31
+ * during the answering pass, that spec wins and the formatting pass is skipped.
32
+ *
33
+ * The conversation id doubles as the agent's memory thread id, so a client that
34
+ * keeps posting the same `conversation.id` gets a continuous conversation - the
35
+ * same mapping a real channel relies on.
36
+ *
37
+ * @module
38
+ */
39
+ import { activity as activityContract, card } from "@dbx-tools/shared-teams";
40
+ /**
41
+ * Minimal structural shape of the Mastra `Agent` this module drives.
42
+ *
43
+ * Declared structurally rather than importing `@mastra/core`'s `Agent`: this
44
+ * package must not depend on the Mastra plugin (the plugin depends on nothing
45
+ * here either), and a turn only ever needs `generate`. Any object with a
46
+ * compatible `generate` satisfies it, which also makes the turn trivially
47
+ * testable with a stub.
48
+ */
49
+ export interface CardAgentLike {
50
+ generate(prompt: string, options: {
51
+ structuredOutput?: {
52
+ schema: typeof card.cardSpecSchema;
53
+ jsonPromptInjection?: boolean | "system" | "inline";
54
+ };
55
+ memory?: {
56
+ thread: string;
57
+ resource: string;
58
+ };
59
+ /**
60
+ * Mastra's per-turn `RequestContext`. Opaque here - the object comes from
61
+ * the agent plugin (see {@link AgentProviderLike.exports}) and is only
62
+ * forwarded - but REQUIRED for parity with the chat endpoints: Mastra's
63
+ * user-scoped tools read the AppKit user off it, so a turn without one
64
+ * answers "the data source is unreachable" where chat answers with data.
65
+ */
66
+ requestContext?: unknown;
67
+ abortSignal?: AbortSignal;
68
+ }): Promise<AgentResult>;
69
+ }
70
+ /**
71
+ * The slice of Mastra's `generate` result a turn reads.
72
+ *
73
+ * `toolResults` matters as much as `object` here: when structured output fails
74
+ * and the turn re-asks as prose, an agent holding the `create_teams_card` tool
75
+ * typically CALLS it, so the best available card spec is the tool's arguments -
76
+ * already in the right vocabulary - rather than anything in the prose.
77
+ */
78
+ export interface AgentResult {
79
+ object?: unknown;
80
+ text?: string;
81
+ toolResults?: {
82
+ payload?: {
83
+ toolName?: string;
84
+ args?: unknown;
85
+ };
86
+ toolName?: string;
87
+ args?: unknown;
88
+ }[];
89
+ }
90
+ /** The bot's identity on outbound activities when the caller names none. */
91
+ export declare const BOT_ACCOUNT: activityContract.ChannelAccount;
92
+ /**
93
+ * Instructions for the FORMATTING pass only - turning an answer the agent has
94
+ * already produced into the card vocabulary.
95
+ *
96
+ * Deliberately NOT sent with the user's question. Asking for a card and an
97
+ * answer in one request makes the model treat formatting as the task: it emits a
98
+ * card immediately instead of calling its tools, so a question that should have
99
+ * queried Genie came back as "I don't have a real system connected - here is a
100
+ * template card". Formatting is a separate, second pass over a real answer.
101
+ */
102
+ export declare const CARD_FORMAT_INSTRUCTIONS: string;
103
+ /**
104
+ * Nudge added to the ANSWERING pass.
105
+ *
106
+ * The agent answers the question normally here - tools included - so this only
107
+ * asks for the shape that survives compression into a card well. It must not
108
+ * mention cards, or the model starts formatting instead of answering.
109
+ */
110
+ export declare const CARD_ANSWER_INSTRUCTIONS: string;
111
+ /**
112
+ * Structural shape of the sibling plugin that owns the agent registry - the
113
+ * slice of `@dbx-tools/appkit-mastra`'s `exports()` a turn needs.
114
+ *
115
+ * Matched structurally, by registered plugin NAME, rather than importing the
116
+ * Mastra plugin: this package stays a leaf add-on (it depends on no other
117
+ * dbx-tools runtime package, like node-email), the dependency direction stays
118
+ * one-way, and any plugin exposing the same `get` / `getDefault` pair can back
119
+ * the endpoint.
120
+ */
121
+ export interface AgentProviderLike {
122
+ exports(): {
123
+ get(id: string): unknown;
124
+ getDefault(): unknown;
125
+ /**
126
+ * Builds the per-turn `RequestContext` the provider's tools expect. Optional
127
+ * so an older provider (or a differently-shaped one) still resolves an
128
+ * agent; the turn simply runs without user-scoped tool context then.
129
+ */
130
+ createRequestContext?(options: {
131
+ threadId?: string;
132
+ resourceId?: string;
133
+ }): Promise<unknown>;
134
+ };
135
+ }
136
+ /**
137
+ * Resolve the agent that answers a turn from the AppKit plugin registry.
138
+ *
139
+ * `agentId` picks a specific agent; omitted, the provider's default agent
140
+ * answers. Returns `null` when the provider is absent or the id is unknown, so
141
+ * the route can answer 503 / 404 instead of throwing.
142
+ */
143
+ export declare const resolveCardAgent: (plugins: ReadonlyMap<string, unknown> | undefined, providerName: string, agentId?: string) => CardAgentLike | null;
144
+ /** Options for {@link runCardTurn}. */
145
+ export interface CardTurnOptions {
146
+ /** Cancels the agent call with the request. */
147
+ signal?: AbortSignal;
148
+ /** Overrides the bot identity stamped on the reply. */
149
+ bot?: activityContract.ChannelAccount;
150
+ /**
151
+ * Builds the Mastra `RequestContext` for the turn, from
152
+ * {@link resolveCardContextFactory}. Omitted, the turn still answers, but
153
+ * without the AppKit user its user-scoped tools cannot reach Databricks - so
154
+ * the route should always supply it.
155
+ */
156
+ createRequestContext?: CardContextFactory;
157
+ }
158
+ /** Builds the per-turn request context the agent's tools read. */
159
+ export type CardContextFactory = (options: {
160
+ threadId?: string;
161
+ resourceId?: string;
162
+ }) => Promise<unknown>;
163
+ /**
164
+ * The agent plugin's request-context factory, when it exposes one.
165
+ *
166
+ * Resolved separately from the agent because it is the piece that gives an
167
+ * out-of-band turn the same tool reach as a chat turn; a provider without it
168
+ * still answers, just without user-scoped tools.
169
+ */
170
+ export declare const resolveCardContextFactory: (plugins: ReadonlyMap<string, unknown> | undefined, providerName: string) => CardContextFactory | null;
171
+ /**
172
+ * The text a `message` activity carries, or `null` when it carries none.
173
+ *
174
+ * A channel sends plenty of activities with no usable text (a `typing`
175
+ * indicator, a `conversationUpdate` when someone joins, or a `message` whose
176
+ * payload is only an attachment). Those are not errors - they simply produce no
177
+ * reply - so this returns `null` rather than throwing.
178
+ */
179
+ export declare const promptOf: (inbound: activityContract.Activity) => string | null;
180
+ /**
181
+ * Build an outbound activity carrying `cards`, addressed back to the sender of
182
+ * `inbound`.
183
+ *
184
+ * Exported because a client rendering an optimistic local reply, and a test
185
+ * asserting the envelope, both need the same construction the turn uses.
186
+ */
187
+ export declare const toReplyActivity: (inbound: activityContract.Activity, cards: card.AdaptiveCard[], options?: {
188
+ bot?: activityContract.ChannelAccount;
189
+ text?: string;
190
+ }) => activityContract.Activity;
191
+ /**
192
+ * Recover a {@link card.CardSpec} from a full Adaptive Card DOCUMENT.
193
+ *
194
+ * A capable model asked for "a Microsoft Teams Adaptive Card" often answers with
195
+ * the finished 1.5 document instead of the small spec - correct Adaptive Card
196
+ * JSON, wrong schema, so `structuredOutput` rejects it (`title: expected
197
+ * string`) and a genuinely good answer is thrown away. Reading the document back
198
+ * into the spec vocabulary keeps it: the heading TextBlocks become
199
+ * title/subtitle, remaining TextBlocks the body, the FactSet the facts, and any
200
+ * `Action.OpenUrl` the actions.
201
+ *
202
+ * Deliberately tolerant about the container: `type` may be missing and the body
203
+ * may nest elements inside a `Container` / `ColumnSet`, so blocks are collected
204
+ * recursively and anything unrecognized is ignored.
205
+ */
206
+ export declare const documentCardSpec: (value: unknown) => card.CardSpec | null;
207
+ /**
208
+ * The card the model produced, wherever it ended up.
209
+ *
210
+ * `structuredOutput` rejects anything off-schema by THROWING, and Mastra puts
211
+ * the offending payload on the error (`details.value`) - so the model's real
212
+ * answer is recoverable from a failed call. Both the spec shape and a full
213
+ * Adaptive Card document are accepted, from an object or from JSON text.
214
+ */
215
+ export declare const rejectedCardSpec: (err: unknown) => card.CardSpec | null;
216
+ /**
217
+ * Run one conversation turn: drive `agent` with the inbound activity's text and
218
+ * return the activities to append to the transcript.
219
+ *
220
+ * Returns an EMPTY array for an activity that carries no prompt (a typing
221
+ * indicator, a join event), which is exactly what a bot does with one - the
222
+ * route answers `{ activities: [] }` and the transcript is unchanged.
223
+ *
224
+ * Two agent calls, in this order:
225
+ *
226
+ * 1. the ANSWER - no `structuredOutput`, so the agent's tools are available
227
+ * and the content matches what the streaming endpoint would say;
228
+ * 2. the FORMAT - {@link formatAsCard} compiling that answer into a card.
229
+ *
230
+ * Every failure mode still yields the answer: a card the agent composed with
231
+ * `create_teams_card` during pass 1 short-circuits pass 2, and a failed pass 2
232
+ * falls back to {@link toCardSpec} over the answer text.
233
+ */
234
+ export declare const runCardTurn: (agent: CardAgentLike, inbound: activityContract.Activity, options?: CardTurnOptions) => Promise<activityContract.Activity[]>;