@copilotkit/channels-teams 0.1.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.
- package/LICENSE +21 -0
- package/README.md +141 -0
- package/dist/adapter.d.ts +133 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +539 -0
- package/dist/adapter.test.d.ts +2 -0
- package/dist/adapter.test.d.ts.map +1 -0
- package/dist/adapter.test.js +202 -0
- package/dist/conversation-store.d.ts +36 -0
- package/dist/conversation-store.d.ts.map +1 -0
- package/dist/conversation-store.js +78 -0
- package/dist/conversation-store.test.d.ts +2 -0
- package/dist/conversation-store.test.d.ts.map +1 -0
- package/dist/conversation-store.test.js +72 -0
- package/dist/download-files.d.ts +62 -0
- package/dist/download-files.d.ts.map +1 -0
- package/dist/download-files.js +190 -0
- package/dist/download-files.test.d.ts +2 -0
- package/dist/download-files.test.d.ts.map +1 -0
- package/dist/download-files.test.js +96 -0
- package/dist/event-renderer.d.ts +23 -0
- package/dist/event-renderer.d.ts.map +1 -0
- package/dist/event-renderer.js +135 -0
- package/dist/event-renderer.test.d.ts +2 -0
- package/dist/event-renderer.test.d.ts.map +1 -0
- package/dist/event-renderer.test.js +95 -0
- package/dist/graph-files.d.ts +50 -0
- package/dist/graph-files.d.ts.map +1 -0
- package/dist/graph-files.js +126 -0
- package/dist/graph-files.test.d.ts +2 -0
- package/dist/graph-files.test.d.ts.map +1 -0
- package/dist/graph-files.test.js +114 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/interaction.d.ts +36 -0
- package/dist/interaction.d.ts.map +1 -0
- package/dist/interaction.js +35 -0
- package/dist/interaction.test.d.ts +2 -0
- package/dist/interaction.test.d.ts.map +1 -0
- package/dist/interaction.test.js +43 -0
- package/dist/listener.d.ts +26 -0
- package/dist/listener.d.ts.map +1 -0
- package/dist/listener.js +55 -0
- package/dist/listener.test.d.ts +2 -0
- package/dist/listener.test.d.ts.map +1 -0
- package/dist/listener.test.js +43 -0
- package/dist/message-stream.d.ts +52 -0
- package/dist/message-stream.d.ts.map +1 -0
- package/dist/message-stream.js +78 -0
- package/dist/message-stream.test.d.ts +2 -0
- package/dist/message-stream.test.d.ts.map +1 -0
- package/dist/message-stream.test.js +44 -0
- package/dist/render/adaptive-card.d.ts +43 -0
- package/dist/render/adaptive-card.d.ts.map +1 -0
- package/dist/render/adaptive-card.js +401 -0
- package/dist/render/adaptive-card.test.d.ts +2 -0
- package/dist/render/adaptive-card.test.d.ts.map +1 -0
- package/dist/render/adaptive-card.test.js +240 -0
- package/dist/render/auto-close.d.ts +27 -0
- package/dist/render/auto-close.d.ts.map +1 -0
- package/dist/render/auto-close.js +153 -0
- package/dist/render/auto-close.test.d.ts +2 -0
- package/dist/render/auto-close.test.d.ts.map +1 -0
- package/dist/render/auto-close.test.js +46 -0
- package/dist/render/budget.d.ts +38 -0
- package/dist/render/budget.d.ts.map +1 -0
- package/dist/render/budget.js +44 -0
- package/dist/render/budget.test.d.ts +2 -0
- package/dist/render/budget.test.d.ts.map +1 -0
- package/dist/render/budget.test.js +25 -0
- package/dist/render/markdown.d.ts +13 -0
- package/dist/render/markdown.d.ts.map +1 -0
- package/dist/render/markdown.js +121 -0
- package/dist/render/markdown.test.d.ts +2 -0
- package/dist/render/markdown.test.d.ts.map +1 -0
- package/dist/render/markdown.test.js +58 -0
- package/dist/sanitizing-http-agent.d.ts +35 -0
- package/dist/sanitizing-http-agent.d.ts.map +1 -0
- package/dist/sanitizing-http-agent.js +58 -0
- package/dist/types.d.ts +53 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +67 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming-polish layer for Teams' post-then-edit streaming.
|
|
3
|
+
*
|
|
4
|
+
* While an agent is mid-stream the buffer is usually an *unfinished* markdown
|
|
5
|
+
* document: an open code fence, an unclosed `**`, etc. Teams renders message
|
|
6
|
+
* text as markdown, so editing the message with unbalanced text makes the rest
|
|
7
|
+
* of the bubble render broken (a stray ``` turns the remainder into a code
|
|
8
|
+
* block until the closer arrives, a dangling `**` bolds the rest, and so on).
|
|
9
|
+
*
|
|
10
|
+
* `autoCloseOpenMarkdown` returns a balanced copy of the buffer by appending the
|
|
11
|
+
* minimum set of closers needed for well-formed display. When the agent later
|
|
12
|
+
* emits the real closer the buffer balances on its own and this adds nothing,
|
|
13
|
+
* so the *committed* (finalized) message has no synthetic closers.
|
|
14
|
+
*
|
|
15
|
+
* Handled, in priority order:
|
|
16
|
+
* 1. Unclosed fenced code blocks ``` (most severe: leaks code styling)
|
|
17
|
+
* 2. Unclosed inline code
|
|
18
|
+
* 3. Unclosed bold `**` / `__`, italic `*` / `_`, strike `~~`
|
|
19
|
+
*
|
|
20
|
+
* Heuristics:
|
|
21
|
+
* - A marker with no content after it (trailing `**`) is NOT closed. The
|
|
22
|
+
* agent may just be opening it; closing would flash a transient `****`.
|
|
23
|
+
* - Markers inside code/fence regions don't count.
|
|
24
|
+
* - Closers are emitted innermost-first so the structure nests correctly.
|
|
25
|
+
*/
|
|
26
|
+
// Private-use codepoints that will never appear in agent markdown, used to
|
|
27
|
+
// stash already-balanced regions while we scan for the dangling opener.
|
|
28
|
+
const SENTINEL_FENCE = "\uE000";
|
|
29
|
+
const SENTINEL_INLINE = "\uE001";
|
|
30
|
+
export function autoCloseOpenMarkdown(text) {
|
|
31
|
+
if (!text)
|
|
32
|
+
return text;
|
|
33
|
+
// ── 1. Fenced code blocks ──────────────────────────────────────────
|
|
34
|
+
const fences = [];
|
|
35
|
+
let work = text.replace(/```[\s\S]*?```/g, (m) => {
|
|
36
|
+
fences.push(m);
|
|
37
|
+
return `${SENTINEL_FENCE}${fences.length - 1}${SENTINEL_FENCE}`;
|
|
38
|
+
});
|
|
39
|
+
const openFenceIdx = work.indexOf("```");
|
|
40
|
+
let openFenceTail = "";
|
|
41
|
+
if (openFenceIdx >= 0) {
|
|
42
|
+
openFenceTail = work.slice(openFenceIdx);
|
|
43
|
+
work = work.slice(0, openFenceIdx);
|
|
44
|
+
}
|
|
45
|
+
// ── 2. Inline code regions ─────────────────────────────────────────
|
|
46
|
+
const inlines = [];
|
|
47
|
+
work = work.replace(/`[^`\n]+`/g, (m) => {
|
|
48
|
+
inlines.push(m);
|
|
49
|
+
return `${SENTINEL_INLINE}${inlines.length - 1}${SENTINEL_INLINE}`;
|
|
50
|
+
});
|
|
51
|
+
const openBacktickIdx = work.indexOf("`");
|
|
52
|
+
let openBacktickTail = "";
|
|
53
|
+
if (openBacktickIdx >= 0) {
|
|
54
|
+
openBacktickTail = work.slice(openBacktickIdx);
|
|
55
|
+
work = work.slice(0, openBacktickIdx);
|
|
56
|
+
}
|
|
57
|
+
// ── 3. Bold / italic / strike via stack scan ──────────────────────
|
|
58
|
+
const stack = scanBracketStack(work);
|
|
59
|
+
const closers = stack.slice().toReversed().join("");
|
|
60
|
+
// ── 4. Reassemble ─────────────────────────────────────────────────
|
|
61
|
+
// Insert closers BEFORE trailing whitespace so "**bold " → "**bold**".
|
|
62
|
+
let output = work;
|
|
63
|
+
if (closers) {
|
|
64
|
+
const trail = output.match(/\s*$/)?.[0] ?? "";
|
|
65
|
+
output = output.slice(0, output.length - trail.length) + closers + trail;
|
|
66
|
+
}
|
|
67
|
+
if (openBacktickTail) {
|
|
68
|
+
output += openBacktickTail;
|
|
69
|
+
if (hasContentAfterMarker(openBacktickTail, "`"))
|
|
70
|
+
output += "`";
|
|
71
|
+
}
|
|
72
|
+
output = output.replace(new RegExp(`${SENTINEL_INLINE}(\\d+)${SENTINEL_INLINE}`, "g"), (_, idx) => inlines[Number(idx)] ?? "");
|
|
73
|
+
if (openFenceTail) {
|
|
74
|
+
output += openFenceTail;
|
|
75
|
+
if (hasFenceCodeContent(openFenceTail)) {
|
|
76
|
+
output += openFenceTail.endsWith("\n") ? "```" : "\n```";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
output = output.replace(new RegExp(`${SENTINEL_FENCE}(\\d+)${SENTINEL_FENCE}`, "g"), (_, idx) => fences[Number(idx)] ?? "");
|
|
80
|
+
return output;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Walk `text` and return the unbalanced bracket stack. Recognises (longest
|
|
84
|
+
* first) `**`, `__`, `~~`, then `*`, `_`, so `**` is one bold marker, not two
|
|
85
|
+
* italics. A trailing marker with no content after it is dropped (closing it
|
|
86
|
+
* would flash a transient `****`).
|
|
87
|
+
*/
|
|
88
|
+
function scanBracketStack(text) {
|
|
89
|
+
const stack = [];
|
|
90
|
+
const tryToggle = (m) => {
|
|
91
|
+
if (stack[stack.length - 1] === m)
|
|
92
|
+
stack.pop();
|
|
93
|
+
else
|
|
94
|
+
stack.push(m);
|
|
95
|
+
};
|
|
96
|
+
let i = 0;
|
|
97
|
+
while (i < text.length) {
|
|
98
|
+
if (text.startsWith("**", i)) {
|
|
99
|
+
tryToggle("**");
|
|
100
|
+
i += 2;
|
|
101
|
+
}
|
|
102
|
+
else if (text.startsWith("__", i)) {
|
|
103
|
+
tryToggle("__");
|
|
104
|
+
i += 2;
|
|
105
|
+
}
|
|
106
|
+
else if (text.startsWith("~~", i)) {
|
|
107
|
+
tryToggle("~~");
|
|
108
|
+
i += 2;
|
|
109
|
+
}
|
|
110
|
+
else if (text[i] === "*") {
|
|
111
|
+
tryToggle("*");
|
|
112
|
+
i += 1;
|
|
113
|
+
}
|
|
114
|
+
else if (text[i] === "_") {
|
|
115
|
+
tryToggle("_");
|
|
116
|
+
i += 1;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
i += 1;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
while (stack.length > 0) {
|
|
123
|
+
const last = stack[stack.length - 1];
|
|
124
|
+
const lastIdx = text.lastIndexOf(last);
|
|
125
|
+
if (lastIdx < 0)
|
|
126
|
+
break;
|
|
127
|
+
const after = text.slice(lastIdx + last.length);
|
|
128
|
+
if (/^\s*$/.test(after))
|
|
129
|
+
stack.pop();
|
|
130
|
+
else
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
return stack;
|
|
134
|
+
}
|
|
135
|
+
function hasContentAfterMarker(tail, marker) {
|
|
136
|
+
if (!tail.startsWith(marker))
|
|
137
|
+
return false;
|
|
138
|
+
return /\S/.test(tail.slice(marker.length));
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* True if a ```` ```<lang>?\n<content> ```` buffer has actual code past the
|
|
142
|
+
* optional language line (the first newline separates the language tag from the
|
|
143
|
+
* code body). A buffer still on the language line has nothing to close yet.
|
|
144
|
+
*/
|
|
145
|
+
function hasFenceCodeContent(tail) {
|
|
146
|
+
if (!tail.startsWith("```"))
|
|
147
|
+
return false;
|
|
148
|
+
const after = tail.slice(3);
|
|
149
|
+
const nl = after.indexOf("\n");
|
|
150
|
+
if (nl < 0)
|
|
151
|
+
return false;
|
|
152
|
+
return /\S/.test(after.slice(nl + 1));
|
|
153
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auto-close.test.d.ts","sourceRoot":"","sources":["../../src/render/auto-close.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { autoCloseOpenMarkdown as ac } from "./auto-close.js";
|
|
3
|
+
describe("autoCloseOpenMarkdown", () => {
|
|
4
|
+
it("leaves empty and already-balanced text untouched (no synthetic closers)", () => {
|
|
5
|
+
expect(ac("")).toBe("");
|
|
6
|
+
expect(ac("plain text")).toBe("plain text");
|
|
7
|
+
expect(ac("**bold** and _italic_")).toBe("**bold** and _italic_");
|
|
8
|
+
expect(ac("```js\nconst x = 1;\n```")).toBe("```js\nconst x = 1;\n```");
|
|
9
|
+
});
|
|
10
|
+
it("closes an unclosed bold/italic/strike that has content", () => {
|
|
11
|
+
expect(ac("**bold")).toBe("**bold**");
|
|
12
|
+
expect(ac("_italic")).toBe("_italic_");
|
|
13
|
+
expect(ac("~~strike")).toBe("~~strike~~");
|
|
14
|
+
});
|
|
15
|
+
it("closes nested markers innermost-first", () => {
|
|
16
|
+
expect(ac("**bold _italic")).toBe("**bold _italic_**");
|
|
17
|
+
});
|
|
18
|
+
it("inserts closers before trailing whitespace", () => {
|
|
19
|
+
expect(ac("**bold ")).toBe("**bold** ");
|
|
20
|
+
});
|
|
21
|
+
it("does NOT close a marker with no content after it (avoids transient ****)", () => {
|
|
22
|
+
expect(ac("hello **")).toBe("hello **");
|
|
23
|
+
expect(ac("text _")).toBe("text _");
|
|
24
|
+
});
|
|
25
|
+
it("closes an open inline code span with content, but not a bare opener", () => {
|
|
26
|
+
expect(ac("run `npm test")).toBe("run `npm test`");
|
|
27
|
+
expect(ac("a paired `span` then more")).toBe("a paired `span` then more");
|
|
28
|
+
});
|
|
29
|
+
it("closes an open fence only once there's code past the language line", () => {
|
|
30
|
+
expect(ac("```js")).toBe("```js"); // still on the language line
|
|
31
|
+
expect(ac("```js\n")).toBe("```js\n"); // language line, no code yet
|
|
32
|
+
expect(ac("```js\nconst x = 1;")).toBe("```js\nconst x = 1;\n```");
|
|
33
|
+
expect(ac("```\nplain code")).toBe("```\nplain code\n```");
|
|
34
|
+
});
|
|
35
|
+
it("does not corrupt digits adjacent to balanced regions (sentinel safety)", () => {
|
|
36
|
+
// Regression guard: the placeholder sentinels must be real PUA codepoints,
|
|
37
|
+
// not empty strings, otherwise the restore regex would eat bare digits.
|
|
38
|
+
expect(ac("```\ncode\n```\n12345 items")).toBe("```\ncode\n```\n12345 items");
|
|
39
|
+
expect(ac("see `x` then 2024 and 99")).toBe("see `x` then 2024 and 99");
|
|
40
|
+
});
|
|
41
|
+
it("is idempotent once the real closer arrives", () => {
|
|
42
|
+
// mid-stream we'd have closed it; the finalized balanced text adds nothing.
|
|
43
|
+
expect(ac("**done**")).toBe("**done**");
|
|
44
|
+
expect(ac(ac("**partial"))).toBe(ac("**partial"));
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adaptive Card payload limits for Teams.
|
|
3
|
+
*
|
|
4
|
+
* Teams caps an Adaptive Card attachment at ~28 KB of JSON and renders only a
|
|
5
|
+
* handful of top-level actions comfortably. These ceilings keep a rendered card
|
|
6
|
+
* inside those bounds; the renderer clamps collections and truncates text to
|
|
7
|
+
* them rather than emitting an oversized card Teams would reject.
|
|
8
|
+
*/
|
|
9
|
+
export declare const TEAMS_LIMITS: {
|
|
10
|
+
/** Top-level body elements (TextBlocks, FactSets, Tables, etc.) per card. */
|
|
11
|
+
readonly bodyElements: 100;
|
|
12
|
+
/** Top-level `Action.Submit`s. Teams shows ~6 before overflowing. */
|
|
13
|
+
readonly actions: 6;
|
|
14
|
+
/** Characters of text in a single TextBlock. */
|
|
15
|
+
readonly textBlock: 12000;
|
|
16
|
+
readonly factTitle: 200;
|
|
17
|
+
readonly factValue: 2000;
|
|
18
|
+
readonly factsPerSet: 50;
|
|
19
|
+
readonly buttonText: 256;
|
|
20
|
+
readonly tableColumns: 12;
|
|
21
|
+
readonly tableRows: 100;
|
|
22
|
+
readonly cellText: 2000;
|
|
23
|
+
readonly choices: 100;
|
|
24
|
+
readonly choiceLabel: 256;
|
|
25
|
+
/** Data points (categories / slices) in a single chart. */
|
|
26
|
+
readonly chartDataPoints: 50;
|
|
27
|
+
/** Characters of a chart title or a data point's label. */
|
|
28
|
+
readonly chartTitle: 200;
|
|
29
|
+
readonly chartLabel: 200;
|
|
30
|
+
};
|
|
31
|
+
/** Truncate to `max` chars, appending an ellipsis if the input was longer. Never returns >max. */
|
|
32
|
+
export declare function truncateText(text: string, max: number): string;
|
|
33
|
+
/** Clamp an array to `max` items; return the kept items plus how many overflowed. */
|
|
34
|
+
export declare function clampArray<T>(items: readonly T[], max: number): {
|
|
35
|
+
items: T[];
|
|
36
|
+
overflow: number;
|
|
37
|
+
};
|
|
38
|
+
//# sourceMappingURL=budget.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"budget.d.ts","sourceRoot":"","sources":["../../src/render/budget.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY;IACvB,6EAA6E;;IAE7E,qEAAqE;;IAErE,gDAAgD;;;;;;;;;;;IAWhD,2DAA2D;;IAE3D,2DAA2D;;;CAGnD,CAAC;AAEX,kGAAkG;AAClG,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAI9D;AAED,qFAAqF;AACrF,wBAAgB,UAAU,CAAC,CAAC,EAC1B,KAAK,EAAE,SAAS,CAAC,EAAE,EACnB,GAAG,EAAE,MAAM,GACV;IAAE,KAAK,EAAE,CAAC,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAGlC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adaptive Card payload limits for Teams.
|
|
3
|
+
*
|
|
4
|
+
* Teams caps an Adaptive Card attachment at ~28 KB of JSON and renders only a
|
|
5
|
+
* handful of top-level actions comfortably. These ceilings keep a rendered card
|
|
6
|
+
* inside those bounds; the renderer clamps collections and truncates text to
|
|
7
|
+
* them rather than emitting an oversized card Teams would reject.
|
|
8
|
+
*/
|
|
9
|
+
export const TEAMS_LIMITS = {
|
|
10
|
+
/** Top-level body elements (TextBlocks, FactSets, Tables, etc.) per card. */
|
|
11
|
+
bodyElements: 100,
|
|
12
|
+
/** Top-level `Action.Submit`s. Teams shows ~6 before overflowing. */
|
|
13
|
+
actions: 6,
|
|
14
|
+
/** Characters of text in a single TextBlock. */
|
|
15
|
+
textBlock: 12000,
|
|
16
|
+
factTitle: 200,
|
|
17
|
+
factValue: 2000,
|
|
18
|
+
factsPerSet: 50,
|
|
19
|
+
buttonText: 256,
|
|
20
|
+
tableColumns: 12,
|
|
21
|
+
tableRows: 100,
|
|
22
|
+
cellText: 2000,
|
|
23
|
+
choices: 100,
|
|
24
|
+
choiceLabel: 256,
|
|
25
|
+
/** Data points (categories / slices) in a single chart. */
|
|
26
|
+
chartDataPoints: 50,
|
|
27
|
+
/** Characters of a chart title or a data point's label. */
|
|
28
|
+
chartTitle: 200,
|
|
29
|
+
chartLabel: 200,
|
|
30
|
+
};
|
|
31
|
+
/** Truncate to `max` chars, appending an ellipsis if the input was longer. Never returns >max. */
|
|
32
|
+
export function truncateText(text, max) {
|
|
33
|
+
if (text.length <= max)
|
|
34
|
+
return text;
|
|
35
|
+
if (max <= 1)
|
|
36
|
+
return text.slice(0, max);
|
|
37
|
+
return text.slice(0, max - 1) + "…";
|
|
38
|
+
}
|
|
39
|
+
/** Clamp an array to `max` items; return the kept items plus how many overflowed. */
|
|
40
|
+
export function clampArray(items, max) {
|
|
41
|
+
if (items.length <= max)
|
|
42
|
+
return { items: [...items], overflow: 0 };
|
|
43
|
+
return { items: items.slice(0, max), overflow: items.length - max };
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"budget.test.d.ts","sourceRoot":"","sources":["../../src/render/budget.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { truncateText, clampArray, TEAMS_LIMITS } from "./budget.js";
|
|
3
|
+
describe("truncateText", () => {
|
|
4
|
+
it("leaves short text unchanged", () => {
|
|
5
|
+
expect(truncateText("hi", 10)).toBe("hi");
|
|
6
|
+
});
|
|
7
|
+
it("truncates with an ellipsis and never exceeds max", () => {
|
|
8
|
+
const out = truncateText("abcdef", 4);
|
|
9
|
+
expect(out).toBe("abc…");
|
|
10
|
+
expect(out.length).toBe(4);
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
describe("clampArray", () => {
|
|
14
|
+
it("keeps everything under the cap", () => {
|
|
15
|
+
expect(clampArray([1, 2], 5)).toEqual({ items: [1, 2], overflow: 0 });
|
|
16
|
+
});
|
|
17
|
+
it("clamps and reports overflow", () => {
|
|
18
|
+
expect(clampArray([1, 2, 3, 4], 2)).toEqual({ items: [1, 2], overflow: 2 });
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
describe("TEAMS_LIMITS", () => {
|
|
22
|
+
it("caps top-level actions to a Teams-friendly count", () => {
|
|
23
|
+
expect(TEAMS_LIMITS.actions).toBe(6);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { BotNode } from "@copilotkit/channels-ui";
|
|
2
|
+
/**
|
|
3
|
+
* Render the bot-ui IR tree to a Teams message string.
|
|
4
|
+
*
|
|
5
|
+
* Teams message activities render Markdown, so the structural vocabulary maps
|
|
6
|
+
* cleanly onto it: a `<Header>` becomes a bold line, `<Section>`/`<Markdown>`
|
|
7
|
+
* pass their text through, `<Divider>` becomes a rule, and so on. This is the
|
|
8
|
+
* thin, text-first renderer that covers plain replies and the common card
|
|
9
|
+
* shapes; richer interactive surfaces (buttons, inputs) will render to
|
|
10
|
+
* Adaptive Cards in a follow-up (see the package README).
|
|
11
|
+
*/
|
|
12
|
+
export declare function renderTeamsMarkdown(ir: BotNode[]): string;
|
|
13
|
+
//# sourceMappingURL=markdown.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/render/markdown.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAEvD;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,OAAO,EAAE,GAAG,MAAM,CAMzD"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render the bot-ui IR tree to a Teams message string.
|
|
3
|
+
*
|
|
4
|
+
* Teams message activities render Markdown, so the structural vocabulary maps
|
|
5
|
+
* cleanly onto it: a `<Header>` becomes a bold line, `<Section>`/`<Markdown>`
|
|
6
|
+
* pass their text through, `<Divider>` becomes a rule, and so on. This is the
|
|
7
|
+
* thin, text-first renderer that covers plain replies and the common card
|
|
8
|
+
* shapes; richer interactive surfaces (buttons, inputs) will render to
|
|
9
|
+
* Adaptive Cards in a follow-up (see the package README).
|
|
10
|
+
*/
|
|
11
|
+
export function renderTeamsMarkdown(ir) {
|
|
12
|
+
return ir
|
|
13
|
+
.map((node) => renderNode(node))
|
|
14
|
+
.filter((s) => s.length > 0)
|
|
15
|
+
.join("\n\n")
|
|
16
|
+
.trim();
|
|
17
|
+
}
|
|
18
|
+
function renderNode(node) {
|
|
19
|
+
if (typeof node.type !== "string") {
|
|
20
|
+
// Components are expanded to intrinsic nodes before render(); a stray
|
|
21
|
+
// function/symbol node carries no renderable text.
|
|
22
|
+
return collectText(node);
|
|
23
|
+
}
|
|
24
|
+
switch (node.type) {
|
|
25
|
+
case "text":
|
|
26
|
+
return String(node.props.value ?? "");
|
|
27
|
+
case "header":
|
|
28
|
+
return `**${collectText(node)}**`;
|
|
29
|
+
case "divider":
|
|
30
|
+
return "---";
|
|
31
|
+
case "context":
|
|
32
|
+
// Supplementary, lower-emphasis text.
|
|
33
|
+
return collectText(node)
|
|
34
|
+
.split("\n")
|
|
35
|
+
.map((line) => (line ? `_${line}_` : line))
|
|
36
|
+
.join("\n");
|
|
37
|
+
case "field":
|
|
38
|
+
return collectText(node);
|
|
39
|
+
case "fields":
|
|
40
|
+
return childNodes(node)
|
|
41
|
+
.map((c) => renderNode(c))
|
|
42
|
+
.filter(Boolean)
|
|
43
|
+
.join("\n");
|
|
44
|
+
case "button":
|
|
45
|
+
// No interactive surface yet, so render the label so the intent is visible.
|
|
46
|
+
return `\`${collectText(node)}\``;
|
|
47
|
+
case "table":
|
|
48
|
+
return renderTable(node);
|
|
49
|
+
case "message":
|
|
50
|
+
case "section":
|
|
51
|
+
case "markdown":
|
|
52
|
+
case "actions":
|
|
53
|
+
default:
|
|
54
|
+
// Containers and unknown nodes: render children, falling back to any
|
|
55
|
+
// direct text.
|
|
56
|
+
return renderChildren(node) || collectText(node);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Markdown-table fallback for `<Table>` (the Adaptive Card renderer emits a
|
|
61
|
+
* native Table; this is the text-surface fallback). Teams renders GFM pipe
|
|
62
|
+
* tables in message text.
|
|
63
|
+
*/
|
|
64
|
+
function renderTable(node) {
|
|
65
|
+
const columns = node.props?.columns;
|
|
66
|
+
const rowNodes = childNodes(node).filter((c) => c.type === "row");
|
|
67
|
+
const dataRows = rowNodes.map((r) => childNodes(r)
|
|
68
|
+
.filter((c) => c.type === "cell")
|
|
69
|
+
.map((c) => collectText(c).replace(/\|/g, "\\|")));
|
|
70
|
+
const width = columns?.length ?? dataRows.reduce((m, r) => Math.max(m, r.length), 0);
|
|
71
|
+
if (width === 0)
|
|
72
|
+
return "";
|
|
73
|
+
const headers = columns
|
|
74
|
+
? columns.map((c) => c.header)
|
|
75
|
+
: Array.from({ length: width }, () => " ");
|
|
76
|
+
const sep = (columns ?? Array.from({ length: width })).map((c) => {
|
|
77
|
+
const align = c?.align;
|
|
78
|
+
if (align === "center")
|
|
79
|
+
return ":---:";
|
|
80
|
+
if (align === "right")
|
|
81
|
+
return "---:";
|
|
82
|
+
return "---";
|
|
83
|
+
});
|
|
84
|
+
const line = (cells) => `| ${Array.from({ length: width }, (_, i) => cells[i] ?? "").join(" | ")} |`;
|
|
85
|
+
return [line(headers), `| ${sep.join(" | ")} |`, ...dataRows.map(line)].join("\n");
|
|
86
|
+
}
|
|
87
|
+
function renderChildren(node) {
|
|
88
|
+
const kids = childNodes(node);
|
|
89
|
+
if (kids.length === 0)
|
|
90
|
+
return "";
|
|
91
|
+
return kids
|
|
92
|
+
.map((c) => renderNode(c))
|
|
93
|
+
.filter((s) => s.length > 0)
|
|
94
|
+
.join("\n\n");
|
|
95
|
+
}
|
|
96
|
+
/** Normalize a node's `children` prop to an array of child nodes. */
|
|
97
|
+
function childNodes(node) {
|
|
98
|
+
const children = node.props?.children;
|
|
99
|
+
if (Array.isArray(children))
|
|
100
|
+
return children;
|
|
101
|
+
if (children && typeof children === "object" && "type" in children) {
|
|
102
|
+
return [children];
|
|
103
|
+
}
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
/** Depth-first collection of a node's descendant text. */
|
|
107
|
+
function collectText(node) {
|
|
108
|
+
const out = [];
|
|
109
|
+
const visit = (n) => {
|
|
110
|
+
if (typeof n.type === "string" && n.type === "text") {
|
|
111
|
+
const value = n.props?.value;
|
|
112
|
+
if (value != null)
|
|
113
|
+
out.push(String(value));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
for (const child of childNodes(n))
|
|
117
|
+
visit(child);
|
|
118
|
+
};
|
|
119
|
+
visit(node);
|
|
120
|
+
return out.join(" ").replace(/\s+/g, " ").trim();
|
|
121
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown.test.d.ts","sourceRoot":"","sources":["../../src/render/markdown.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { renderTeamsMarkdown } from "./markdown.js";
|
|
3
|
+
const text = (value) => ({ type: "text", props: { value } });
|
|
4
|
+
describe("renderTeamsMarkdown", () => {
|
|
5
|
+
it("renders a bare text node", () => {
|
|
6
|
+
expect(renderTeamsMarkdown([text("Echo: hi")])).toBe("Echo: hi");
|
|
7
|
+
});
|
|
8
|
+
it("renders a header as bold", () => {
|
|
9
|
+
const ir = [
|
|
10
|
+
{ type: "header", props: { children: [text("Title")] } },
|
|
11
|
+
];
|
|
12
|
+
expect(renderTeamsMarkdown(ir)).toBe("**Title**");
|
|
13
|
+
});
|
|
14
|
+
it("renders a divider as a rule", () => {
|
|
15
|
+
expect(renderTeamsMarkdown([{ type: "divider", props: {} }])).toBe("---");
|
|
16
|
+
});
|
|
17
|
+
it("joins a message container's children with blank lines", () => {
|
|
18
|
+
const ir = [
|
|
19
|
+
{
|
|
20
|
+
type: "message",
|
|
21
|
+
props: {
|
|
22
|
+
children: [
|
|
23
|
+
{ type: "header", props: { children: [text("Status")] } },
|
|
24
|
+
{ type: "section", props: { children: [text("All good.")] } },
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
expect(renderTeamsMarkdown(ir)).toBe("**Status**\n\nAll good.");
|
|
30
|
+
});
|
|
31
|
+
it("renders context children as emphasized lines", () => {
|
|
32
|
+
const ir = [
|
|
33
|
+
{ type: "context", props: { children: [text("fyi")] } },
|
|
34
|
+
];
|
|
35
|
+
expect(renderTeamsMarkdown(ir)).toBe("_fyi_");
|
|
36
|
+
});
|
|
37
|
+
it("returns an empty string for an empty tree", () => {
|
|
38
|
+
expect(renderTeamsMarkdown([])).toBe("");
|
|
39
|
+
});
|
|
40
|
+
it("renders a <Table> as a GFM pipe-table fallback", () => {
|
|
41
|
+
const cell = (v) => ({
|
|
42
|
+
type: "cell",
|
|
43
|
+
props: { children: [text(v)] },
|
|
44
|
+
});
|
|
45
|
+
const ir = [
|
|
46
|
+
{
|
|
47
|
+
type: "table",
|
|
48
|
+
props: {
|
|
49
|
+
columns: [{ header: "Name" }, { header: "Count", align: "right" }],
|
|
50
|
+
children: [
|
|
51
|
+
{ type: "row", props: { children: [cell("Bugs"), cell("3")] } },
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
expect(renderTeamsMarkdown(ir)).toBe("| Name | Count |\n| --- | ---: |\n| Bugs | 3 |");
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { HttpAgent } from "@ag-ui/client";
|
|
2
|
+
import type { BaseEvent, RunAgentInput } from "@ag-ui/core";
|
|
3
|
+
import type { Observable } from "rxjs";
|
|
4
|
+
/**
|
|
5
|
+
* An `HttpAgent` that tolerates the AG-UI event streams real agents emit.
|
|
6
|
+
*
|
|
7
|
+
* `@ag-ui/client`'s stock transform re-validates every streamed event against a
|
|
8
|
+
* strict Zod schema. Some events that `@ag-ui/langgraph` legitimately emits fail
|
|
9
|
+
* it (notably a `TOOL_CALL_START` whose `parentMessageId` is `null`: "Expected
|
|
10
|
+
* string, received null"), and a single rejected event aborts the entire run.
|
|
11
|
+
* That breaks LangGraph interrupts / human-in-the-loop on Teams, where the tool
|
|
12
|
+
* call that triggers the interrupt carries exactly that shape.
|
|
13
|
+
*
|
|
14
|
+
* The bridge talks to a trusted runtime, so rather than re-validate its output
|
|
15
|
+
* we use the same SSE parse the stock path wraps (`parseSSEStream`) and coerce
|
|
16
|
+
* the known nullable-string fields. This deliberately drops the stock
|
|
17
|
+
* transform's *entire* strict Zod re-validation step (not just the offending
|
|
18
|
+
* field). This is acceptable only because the runtime is trusted and `runHttpRequest`
|
|
19
|
+
* still throws on transport/HTTP errors. The first coercion logs a one-time
|
|
20
|
+
* breadcrumb so the workaround is visible in production. Revert to the stock
|
|
21
|
+
* transform (`transformHttpEventStream`) once upstream makes the fields
|
|
22
|
+
* nullable.
|
|
23
|
+
*
|
|
24
|
+
* Use it in place of `HttpAgent` when pointing the Teams bot at a LangGraph
|
|
25
|
+
* (or other AG-UI) agent:
|
|
26
|
+
*
|
|
27
|
+
* ```ts
|
|
28
|
+
* import { SanitizingHttpAgent } from "@copilotkit/channels-teams";
|
|
29
|
+
* const agent = new SanitizingHttpAgent({ url: process.env.AGENT_URL! });
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export declare class SanitizingHttpAgent extends HttpAgent {
|
|
33
|
+
run(input: RunAgentInput): Observable<BaseEvent>;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=sanitizing-http-agent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitizing-http-agent.d.ts","sourceRoot":"","sources":["../src/sanitizing-http-agent.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAkC,MAAM,eAAe,CAAC;AAC1E,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAEvC;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,qBAAa,mBAAoB,SAAQ,SAAS;IAChD,GAAG,CAAC,KAAK,EAAE,aAAa,GAAG,UAAU,CAAC,SAAS,CAAC;CAMjD"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { HttpAgent, parseSSEStream, runHttpRequest } from "@ag-ui/client";
|
|
2
|
+
import { map } from "rxjs";
|
|
3
|
+
/**
|
|
4
|
+
* An `HttpAgent` that tolerates the AG-UI event streams real agents emit.
|
|
5
|
+
*
|
|
6
|
+
* `@ag-ui/client`'s stock transform re-validates every streamed event against a
|
|
7
|
+
* strict Zod schema. Some events that `@ag-ui/langgraph` legitimately emits fail
|
|
8
|
+
* it (notably a `TOOL_CALL_START` whose `parentMessageId` is `null`: "Expected
|
|
9
|
+
* string, received null"), and a single rejected event aborts the entire run.
|
|
10
|
+
* That breaks LangGraph interrupts / human-in-the-loop on Teams, where the tool
|
|
11
|
+
* call that triggers the interrupt carries exactly that shape.
|
|
12
|
+
*
|
|
13
|
+
* The bridge talks to a trusted runtime, so rather than re-validate its output
|
|
14
|
+
* we use the same SSE parse the stock path wraps (`parseSSEStream`) and coerce
|
|
15
|
+
* the known nullable-string fields. This deliberately drops the stock
|
|
16
|
+
* transform's *entire* strict Zod re-validation step (not just the offending
|
|
17
|
+
* field). This is acceptable only because the runtime is trusted and `runHttpRequest`
|
|
18
|
+
* still throws on transport/HTTP errors. The first coercion logs a one-time
|
|
19
|
+
* breadcrumb so the workaround is visible in production. Revert to the stock
|
|
20
|
+
* transform (`transformHttpEventStream`) once upstream makes the fields
|
|
21
|
+
* nullable.
|
|
22
|
+
*
|
|
23
|
+
* Use it in place of `HttpAgent` when pointing the Teams bot at a LangGraph
|
|
24
|
+
* (or other AG-UI) agent:
|
|
25
|
+
*
|
|
26
|
+
* ```ts
|
|
27
|
+
* import { SanitizingHttpAgent } from "@copilotkit/channels-teams";
|
|
28
|
+
* const agent = new SanitizingHttpAgent({ url: process.env.AGENT_URL! });
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export class SanitizingHttpAgent extends HttpAgent {
|
|
32
|
+
run(input) {
|
|
33
|
+
return parseSSEStream(runHttpRequest(() => this.fetch(this.url, this.requestInit(input))), this.debugLogger).pipe(map((event) => coerceNullStrings(event)));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** One-time breadcrumb so the workaround's use is visible in production. */
|
|
37
|
+
let coercionWarned = false;
|
|
38
|
+
/**
|
|
39
|
+
* Coerce known nullable-string event fields to `""`. Targeted on purpose: we
|
|
40
|
+
* only touch fields where a `null` is known to come through from
|
|
41
|
+
* `@ag-ui/langgraph` and would otherwise trip a downstream string check.
|
|
42
|
+
*/
|
|
43
|
+
function coerceNullStrings(event) {
|
|
44
|
+
if (!event || typeof event !== "object")
|
|
45
|
+
return event;
|
|
46
|
+
const e = event;
|
|
47
|
+
if (e["parentMessageId"] === null) {
|
|
48
|
+
e["parentMessageId"] = "";
|
|
49
|
+
if (!coercionWarned) {
|
|
50
|
+
coercionWarned = true;
|
|
51
|
+
console.warn('[SanitizingHttpAgent] coerced a null `parentMessageId` to "" and ' +
|
|
52
|
+
"bypassed @ag-ui/client strict event re-validation for this stream " +
|
|
53
|
+
"(known @ag-ui/langgraph quirk). Remove this agent once upstream " +
|
|
54
|
+
"makes the field nullable.");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return e;
|
|
58
|
+
}
|