Skip to content

Commit d830e19

Browse files
committed
Merge remote-tracking branch 'origin/main' into matrix-sse-cs-7916
# Conflicts: # packages/base/matrix-event.gts # packages/realm-server/tests/realm-endpoints-test.ts
2 parents 95cb256 + 4873320 commit d830e19

File tree

77 files changed

+4862
-2558
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

77 files changed

+4862
-2558
lines changed

.github/workflows/pr-boxel-host.yml

-2
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ jobs:
6363
AWS_S3_BUCKET: boxel-host-preview.stack.cards
6464
AWS_REGION: us-east-1
6565
AWS_CLOUDFRONT_DISTRIBUTION: EU4RGLH4EOCHJ
66-
AI_ASSISTANT_EXPERIMENTAL_ATTACHING_FILES_ENABLED: true
6766
with:
6867
package: boxel-host
6968
environment: staging
@@ -93,7 +92,6 @@ jobs:
9392
AWS_S3_BUCKET: boxel-host-preview.boxel.ai
9493
AWS_REGION: us-east-1
9594
AWS_CLOUDFRONT_DISTRIBUTION: E2PZR9CIAW093B
96-
AI_ASSISTANT_EXPERIMENTAL_ATTACHING_FILES_ENABLED: true
9795
with:
9896
package: boxel-host
9997
environment: production

packages/ai-bot/helpers.ts

+42-2
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@ import type {
1212
SkillsConfigEvent,
1313
ActiveLLMEvent,
1414
CommandResultEvent,
15+
CommandDefinitionsEvent,
1516
} from 'https://cardstack.com/base/matrix-event';
1617
import { MatrixEvent, type IRoomEvent } from 'matrix-js-sdk';
1718
import { ChatCompletionMessageToolCall } from 'openai/resources/chat/completions';
1819
import * as Sentry from '@sentry/node';
1920
import { logger } from '@cardstack/runtime-common';
2021
import {
22+
APP_BOXEL_COMMAND_DEFINITIONS_MSGTYPE,
2123
APP_BOXEL_COMMAND_RESULT_EVENT_TYPE,
2224
APP_BOXEL_COMMAND_RESULT_WITH_OUTPUT_MSGTYPE,
2325
} from '../runtime-common/matrix-constants';
@@ -87,7 +89,7 @@ export async function getPromptParts(
8789
cardFragments,
8890
);
8991
let skills = getEnabledSkills(eventList, cardFragments);
90-
let tools = getTools(history, aiBotUserId);
92+
let tools = getTools(history, skills, aiBotUserId);
9193
let toolChoice = getToolChoice(history, aiBotUserId);
9294
let messages = await getModifyPrompt(history, aiBotUserId, tools, skills);
9395
let model = getModel(eventList);
@@ -430,10 +432,45 @@ export function attachedFilesToPrompt(
430432

431433
export function getTools(
432434
history: DiscreteMatrixEvent[],
435+
enabledSkills: LooseCardResource[],
433436
aiBotUserId: string,
434437
): Tool[] {
435438
// Build map directly from messages
439+
let enabledCommandNames = new Set<string>();
436440
let toolMap = new Map<string, Tool>();
441+
442+
// Get the list of all names from enabled skills
443+
for (let skill of enabledSkills) {
444+
if (skill.attributes?.commands) {
445+
let { commands } = skill.attributes;
446+
for (let command of commands) {
447+
enabledCommandNames.add(command.functionName);
448+
}
449+
}
450+
}
451+
452+
// Iterate over the command definitions, and add any tools that are in
453+
// enabled skills to the tool map
454+
let commandDefinitionEvents: CommandDefinitionsEvent[] = history.filter(
455+
(event) =>
456+
event.type === 'm.room.message' &&
457+
event.content.msgtype === APP_BOXEL_COMMAND_DEFINITIONS_MSGTYPE,
458+
) as CommandDefinitionsEvent[];
459+
460+
for (let event of commandDefinitionEvents) {
461+
let { content } = event;
462+
let { commandDefinitions } = content.data;
463+
for (let commandDefinition of commandDefinitions) {
464+
if (enabledCommandNames.has(commandDefinition.tool.function.name)) {
465+
toolMap.set(
466+
commandDefinition.tool.function.name,
467+
commandDefinition.tool,
468+
);
469+
}
470+
}
471+
}
472+
473+
// Add in tools from the user's messages
437474
for (let event of history) {
438475
if (event.type !== 'm.room.message' || event.sender == aiBotUserId) {
439476
continue;
@@ -719,7 +756,10 @@ export function isCommandResultEvent(
719756
export function eventRequiresResponse(event: MatrixEvent) {
720757
// If it's a message, we should respond unless it's a card fragment
721758
if (event.getType() === 'm.room.message') {
722-
if (event.getContent().msgtype === APP_BOXEL_CARDFRAGMENT_MSGTYPE) {
759+
if (
760+
event.getContent().msgtype === APP_BOXEL_CARDFRAGMENT_MSGTYPE ||
761+
event.getContent().msgtype === APP_BOXEL_COMMAND_DEFINITIONS_MSGTYPE
762+
) {
723763
return false;
724764
}
725765
return true;

packages/ai-bot/tests/prompt-construction-test.ts

+63-25
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import type {
2424
Tool,
2525
CardMessageContent,
2626
} from 'https://cardstack.com/base/matrix-event';
27-
import { EventStatus, IRoomEvent } from 'matrix-js-sdk';
27+
import { EventStatus } from 'matrix-js-sdk';
2828
import { CardDef } from 'https://cardstack.com/base/card-api';
2929
import { readFileSync } from 'fs-extra';
3030
import * as path from 'path';
@@ -961,7 +961,7 @@ example.pdf: Unsupported file type: application/pdf. For now, only text files ar
961961
},
962962
];
963963

964-
const functions = getTools(history, '@aibot:localhost');
964+
const functions = getTools(history, [], '@aibot:localhost');
965965
assert.equal(functions.length, 1);
966966
assert.deepEqual(functions[0], {
967967
type: 'function',
@@ -1031,7 +1031,7 @@ example.pdf: Unsupported file type: application/pdf. For now, only text files ar
10311031
},
10321032
];
10331033

1034-
const functions = getTools(history, '@aibot:localhost');
1034+
const functions = getTools(history, [], '@aibot:localhost');
10351035
assert.equal(functions.length, 1);
10361036
assert.deepEqual(functions[0], {
10371037
type: 'function',
@@ -1143,7 +1143,7 @@ example.pdf: Unsupported file type: application/pdf. For now, only text files ar
11431143
},
11441144
];
11451145

1146-
const functions = getTools(history, '@aibot:localhost');
1146+
const functions = getTools(history, [], '@aibot:localhost');
11471147
assert.equal(functions.length, 1);
11481148
if (functions.length > 0) {
11491149
assert.deepEqual(functions[0], {
@@ -1335,13 +1335,15 @@ test('if tool calls are required, ensure they are set', async () => {
13351335
});
13361336

13371337
test('Create search function calls', () => {
1338-
const history: IRoomEvent[] = [
1338+
const history: DiscreteMatrixEvent[] = [
13391339
{
13401340
type: 'm.room.message',
1341+
room_id: 'room-id-1',
13411342
sender: '@ian:localhost',
13421343
content: {
13431344
msgtype: APP_BOXEL_MESSAGE_MSGTYPE,
13441345
body: 'set the name to dave',
1346+
format: 'org.matrix.custom.html',
13451347
formatted_body: '<p>set the name to dave</p>\n',
13461348
data: {
13471349
context: {
@@ -1354,19 +1356,20 @@ test('Create search function calls', () => {
13541356
origin_server_ts: 1696813813166,
13551357
unsigned: {
13561358
age: 115498,
1359+
transaction_id: 'm1722242836705.8',
13571360
},
13581361
event_id: '$AZ65GbUls1UdpiOPD_AfSVu8RyiFYN1vltmUKmUnV4c',
1359-
age: 115498,
1362+
status: EventStatus.SENT,
13601363
},
13611364
];
13621365

1363-
const functions = getTools(history, '@aibot:localhost');
1366+
const functions = getTools(history, [], '@aibot:localhost');
13641367
assert.equal(functions.length, 1);
13651368
assert.deepEqual(functions[0], getSearchTool());
13661369
});
13671370

13681371
test('Return host result of tool call back to open ai', async () => {
1369-
const history: IRoomEvent[] = [
1372+
const history: DiscreteMatrixEvent[] = [
13701373
{
13711374
type: 'm.room.message',
13721375
room_id: 'room-id-1',
@@ -1479,9 +1482,10 @@ test('Return host result of tool call back to open ai', async () => {
14791482
origin_server_ts: 1722242833562,
14801483
unsigned: {
14811484
age: 20470,
1485+
transaction_id: 'm1722242836705.1',
14821486
},
14831487
event_id: '$p_NQ4tvokzQrIkT24Wj08mdAxBBvmdLOz6ph7UQfMDw',
1484-
age: 20470,
1488+
status: EventStatus.SENT,
14851489
},
14861490
{
14871491
type: 'm.room.message',
@@ -1493,13 +1497,6 @@ test('Return host result of tool call back to open ai', async () => {
14931497
formatted_body:
14941498
'It looks like you want to search for card instances based on the "Author" card you provided. Just for clarity, would you like to search for more cards based on the "Author" module type or something else specific?\n\nFor example, do you want to find all card instances of type "Author" or a different type of card/module?',
14951499
format: 'org.matrix.custom.html',
1496-
'm.new_content': {
1497-
body: 'It looks like you want to search for card instances based on the "Author" card you provided. Just for clarity, would you like to search for more cards based on the "Author" module type or something else specific?\n\nFor example, do you want to find all card instances of type "Author" or a different type of card/module?',
1498-
msgtype: 'm.text',
1499-
formatted_body:
1500-
'It looks like you want to search for card instances based on the "Author" card you provided. Just for clarity, would you like to search for more cards based on the "Author" module type or something else specific?\n\nFor example, do you want to find all card instances of type "Author" or a different type of card/module?',
1501-
format: 'org.matrix.custom.html',
1502-
},
15031500
isStreamingFinished: true,
15041501
'm.relates_to': {
15051502
rel_type: 'm.replace',
@@ -1509,10 +1506,10 @@ test('Return host result of tool call back to open ai', async () => {
15091506
origin_server_ts: 1722242836727,
15101507
unsigned: {
15111508
age: 17305,
1512-
transaction_id: 'm1722242836705.8',
1509+
transaction_id: 'm1722242836705.2',
15131510
},
15141511
event_id: 'message-event-id-1',
1515-
age: 17305,
1512+
status: EventStatus.SENT,
15161513
},
15171514
{
15181515
type: 'm.room.message',
@@ -1626,9 +1623,10 @@ test('Return host result of tool call back to open ai', async () => {
16261623
origin_server_ts: 1722242847418,
16271624
unsigned: {
16281625
age: 6614,
1626+
transaction_id: 'm1722242836705.3',
16291627
},
16301628
event_id: '$FO2XfB0xFiTpm5FmOUiWQqFh_DPQSr4zix41Vj3eqNc',
1631-
age: 6614,
1629+
status: EventStatus.SENT,
16321630
},
16331631
{
16341632
type: 'm.room.message',
@@ -1667,7 +1665,7 @@ test('Return host result of tool call back to open ai', async () => {
16671665
transaction_id: 'm1722242849075.10',
16681666
},
16691667
event_id: 'command-event-id-1',
1670-
age: 4938,
1668+
status: EventStatus.SENT,
16711669
},
16721670
{
16731671
type: APP_BOXEL_COMMAND_RESULT_EVENT_TYPE,
@@ -1681,7 +1679,8 @@ test('Return host result of tool call back to open ai', async () => {
16811679
},
16821680
msgtype: APP_BOXEL_COMMAND_RESULT_WITH_OUTPUT_MSGTYPE,
16831681
data: {
1684-
card: JSON.stringify({
1682+
cardEventId: 'command-result-id-1',
1683+
card: {
16851684
data: {
16861685
type: 'card',
16871686
attributes: {
@@ -1707,23 +1706,30 @@ test('Return host result of tool call back to open ai', async () => {
17071706
},
17081707
],
17091708
},
1709+
meta: {
1710+
adoptsFrom: {
1711+
module: 'https://cardstack.com/base/search-results',
1712+
name: 'SearchResults',
1713+
},
1714+
},
17101715
},
1711-
}),
1716+
},
17121717
},
17131718
},
17141719
origin_server_ts: 1722242853988,
17151720
unsigned: {
17161721
age: 44,
1722+
transaction_id: 'm1722242836705.4',
17171723
},
17181724
event_id: 'command-result-id-1',
1719-
age: 44,
1725+
status: EventStatus.SENT,
17201726
},
17211727
];
1722-
const tools = getTools(history, '@ai-bot:localhost');
1728+
const tools = getTools(history, [], '@ai-bot:localhost');
17231729
const result = await getModifyPrompt(history, '@ai-bot:localhost', tools);
17241730
assert.equal(result[5].role, 'tool');
17251731
assert.equal(result[5].tool_call_id, 'tool-call-id-1');
1726-
const expected = `Command applied, with result card: "{\\"data\\":{\\"type\\":\\"card\\",\\"attributes\\":{\\"title\\":\\"Search Results\\",\\"description\\":\\"Here are the search results\\",\\"results\\":[{\\"data\\":{\\"type\\":\\"card\\",\\"id\\":\\"http://localhost:4201/drafts/Author/1\\",\\"attributes\\":{\\"firstName\\":\\"Alice\\",\\"lastName\\":\\"Enwunder\\",\\"photo\\":null,\\"body\\":\\"Alice is a software engineer at Google.\\",\\"description\\":null,\\"thumbnailURL\\":null},\\"meta\\":{\\"adoptsFrom\\":{\\"module\\":\\"../author\\",\\"name\\":\\"Author\\"}}}}]}}}".`;
1732+
const expected = `Command applied, with result card: {"data":{"type":"card","attributes":{"title":"Search Results","description":"Here are the search results","results":[{"data":{"type":"card","id":"http://localhost:4201/drafts/Author/1","attributes":{"firstName":"Alice","lastName":"Enwunder","photo":null,"body":"Alice is a software engineer at Google.","description":null,"thumbnailURL":null},"meta":{"adoptsFrom":{"module":"../author","name":"Author"}}}}]},"meta":{"adoptsFrom":{"module":"https://cardstack.com/base/search-results","name":"SearchResults"}}}}.`;
17271733

17281734
assert.equal(result[5].content!.trim(), expected.trim());
17291735
});
@@ -1806,6 +1812,38 @@ test('Tools calls are connected to their results', async () => {
18061812
);
18071813
});
18081814

1815+
test('Tools on enabled skills are available in prompt', async () => {
1816+
const eventList: DiscreteMatrixEvent[] = JSON.parse(
1817+
readFileSync(
1818+
path.join(__dirname, 'resources/chats/enabled-skill-with-commands.json'),
1819+
),
1820+
);
1821+
1822+
const { tools } = await getPromptParts(eventList, '@aibot:localhost');
1823+
assert.true(tools.length > 0, 'Should have tools available');
1824+
1825+
// Verify that the tools array contains the command from the skill
1826+
const switchSubmodeTool = tools.find(
1827+
(tool) => tool.function?.name === 'switch-submode_dd88',
1828+
);
1829+
assert.ok(
1830+
switchSubmodeTool,
1831+
'Should have SwitchSubmodeCommand function available',
1832+
);
1833+
});
1834+
1835+
test('No tools are available if skill is not enabled', async () => {
1836+
const eventList: DiscreteMatrixEvent[] = JSON.parse(
1837+
readFileSync(
1838+
path.join(__dirname, 'resources/chats/disabled-skill-with-commands.json'),
1839+
),
1840+
);
1841+
1842+
const { tools } = await getPromptParts(eventList, '@aibot:localhost');
1843+
// we should not have any tools available
1844+
assert.true(tools.length == 0, 'Should not have tools available');
1845+
});
1846+
18091847
module('set model in prompt', () => {
18101848
test('default active LLM must be equal to `DEFAULT_LLM`', async () => {
18111849
const eventList: DiscreteMatrixEvent[] = JSON.parse(

0 commit comments

Comments
 (0)