Skip to content

Commit ae0de84

Browse files
authored
Merge pull request #2132 from cardstack/return-of-the-skill-commands
Commands on skill cards
2 parents 62f68bc + e218961 commit ae0de84

File tree

8 files changed

+211
-19
lines changed

8 files changed

+211
-19
lines changed

packages/host/app/lib/matrix-classes/message-builder.ts

+19-10
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { inject as service } from '@ember/service';
66

77
import {
88
LooseSingleCardDocument,
9+
ResolvedCodeRef,
910
sanitizeHtml,
1011
} from '@cardstack/runtime-common';
1112

@@ -16,12 +17,12 @@ import {
1617
APP_BOXEL_MESSAGE_MSGTYPE,
1718
} from '@cardstack/runtime-common/matrix-constants';
1819

20+
import { Skill } from '@cardstack/host/components/ai-assistant/skill-menu';
1921
import type CommandService from '@cardstack/host/services/command-service';
2022

2123
import MatrixService from '@cardstack/host/services/matrix-service';
2224

2325
import type { CommandStatus } from 'https://cardstack.com/base/command';
24-
2526
import { SerializedFile } from 'https://cardstack.com/base/file-api';
2627
import type {
2728
CardMessageContent,
@@ -50,6 +51,7 @@ export default class MessageBuilder {
5051
author: RoomMember;
5152
index: number;
5253
serializedCardFromFragments: (eventId: string) => LooseSingleCardDocument;
54+
skills: Skill[];
5355
events: DiscreteMatrixEvent[];
5456
commandResultEvent?: CommandResultEvent;
5557
},
@@ -156,10 +158,6 @@ export default class MessageBuilder {
156158
}
157159

158160
updateMessage(message: Message) {
159-
if (this.event.content['m.relates_to']?.rel_type !== 'm.replace') {
160-
return;
161-
}
162-
163161
if (message.created.getTime() > this.event.origin_server_ts) {
164162
message.created = new Date(this.event.origin_server_ts);
165163
return;
@@ -173,11 +171,11 @@ export default class MessageBuilder {
173171
: undefined;
174172
message.updated = new Date();
175173

176-
if (this.event.content.msgtype === APP_BOXEL_COMMAND_MSGTYPE) {
177-
if (!message.command) {
178-
message.command = this.buildMessageCommand(message);
179-
}
180-
174+
if (
175+
this.event.content.msgtype === APP_BOXEL_COMMAND_MSGTYPE &&
176+
this.event.content.data?.toolCall
177+
) {
178+
message.command = this.buildMessageCommand(message);
181179
message.isStreamingFinished = true;
182180
message.formattedMessage = this.formattedMessageForCommand;
183181

@@ -219,11 +217,22 @@ export default class MessageBuilder {
219217
}) as CommandResultEvent | undefined);
220218

221219
let command = event.content.data.toolCall;
220+
// Find command in skills
221+
let codeRef: ResolvedCodeRef | undefined;
222+
findCommand: for (let skill of this.builderContext.skills) {
223+
for (let skillCommand of skill.card.commands) {
224+
if (command.name === skillCommand.functionName) {
225+
codeRef = skillCommand.codeRef;
226+
break findCommand;
227+
}
228+
}
229+
}
222230
let messageCommand = new MessageCommand(
223231
message,
224232
command.id,
225233
command.name,
226234
command.arguments,
235+
codeRef,
227236
this.builderContext.effectiveEventId,
228237
(commandResultEvent?.content['m.relates_to']?.key ||
229238
'ready') as CommandStatus,

packages/host/app/lib/matrix-classes/message-command.ts

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { inject as service } from '@ember/service';
44

55
import { tracked } from '@glimmer/tracking';
66

7+
import { ResolvedCodeRef } from '@cardstack/runtime-common';
8+
79
import type CardService from '@cardstack/host/services/card-service';
810
import type CommandService from '@cardstack/host/services/command-service';
911
import type MatrixService from '@cardstack/host/services/matrix-service';
@@ -25,6 +27,7 @@ export default class MessageCommand {
2527
public toolCallId: string,
2628
name: string,
2729
payload: any, //arguments of toolCall. Its not called arguments due to lint
30+
public codeRef: ResolvedCodeRef | undefined,
2831
public eventId: string,
2932
commandStatus: CommandStatus,
3033
commandResultCardEventId: string | undefined,

packages/host/app/resources/room.ts

+30-3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { type LooseSingleCardDocument } from '@cardstack/runtime-common';
1212
import {
1313
APP_BOXEL_CARDFRAGMENT_MSGTYPE,
1414
APP_BOXEL_COMMAND_MSGTYPE,
15+
APP_BOXEL_COMMAND_DEFINITIONS_MSGTYPE,
1516
APP_BOXEL_COMMAND_RESULT_EVENT_TYPE,
1617
DEFAULT_LLM,
1718
} from '@cardstack/runtime-common/matrix-constants';
@@ -31,9 +32,8 @@ import type {
3132
CommandResultEvent,
3233
} from 'https://cardstack.com/base/matrix-event';
3334

34-
import { SkillCard } from 'https://cardstack.com/base/skill-card';
35+
import type { SkillCard } from 'https://cardstack.com/base/skill-card';
3536

36-
import { Skill } from '../components/ai-assistant/skill-menu';
3737
import {
3838
RoomMember,
3939
type RoomMemberInterface,
@@ -42,6 +42,8 @@ import { Message } from '../lib/matrix-classes/message';
4242

4343
import MessageBuilder from '../lib/matrix-classes/message-builder';
4444

45+
import type { Skill } from '../components/ai-assistant/skill-menu';
46+
4547
import type Room from '../lib/matrix-classes/room';
4648

4749
import type CardService from '../services/card-service';
@@ -57,9 +59,9 @@ interface Args {
5759

5860
export class RoomResource extends Resource<Args> {
5961
private _messageCache: TrackedMap<string, Message> = new TrackedMap();
60-
private _skillCardsCache: TrackedMap<string, SkillCard> = new TrackedMap();
6162
private _skillEventIdToCardIdCache: TrackedMap<string, string> =
6263
new TrackedMap();
64+
private _skillCardsCache: TrackedMap<string, SkillCard> = new TrackedMap();
6365
private _nameEventsCache: TrackedMap<string, RoomNameEvent> =
6466
new TrackedMap();
6567
@tracked private _createEvent: RoomCreateEvent | undefined;
@@ -112,6 +114,8 @@ export class RoomResource extends Resource<Args> {
112114
case 'm.room.message':
113115
if (this.isCardFragmentEvent(event)) {
114116
await this.loadCardFragment(event);
117+
} else if (this.isCommandDefinitionsEvent(event)) {
118+
break;
115119
} else {
116120
await this.loadRoomMessage({
117121
roomId,
@@ -224,6 +228,17 @@ export class RoomResource extends Resource<Args> {
224228
return result;
225229
}
226230

231+
get commands() {
232+
// Usable commands are all commands on *active* skills
233+
let commands = [];
234+
for (let skill of this.skills) {
235+
if (skill.isActive) {
236+
commands.push(...skill.card.commands);
237+
}
238+
}
239+
return commands;
240+
}
241+
227242
@cached
228243
get created() {
229244
if (this._createEvent) {
@@ -302,6 +317,16 @@ export class RoomResource extends Resource<Args> {
302317
});
303318
}
304319

320+
private isCommandDefinitionsEvent(
321+
event:
322+
| MessageEvent
323+
| CommandEvent
324+
| CardMessageEvent
325+
| CommandDefinitionsEvent,
326+
): event is CommandDefinitionsEvent {
327+
return event.content.msgtype === APP_BOXEL_COMMAND_DEFINITIONS_MSGTYPE;
328+
}
329+
305330
private isCardFragmentEvent(
306331
event:
307332
| MessageEvent
@@ -374,6 +399,7 @@ export class RoomResource extends Resource<Args> {
374399
index,
375400
serializedCardFromFragments: this.serializedCardFromFragments,
376401
events: this.events,
402+
skills: this.skills,
377403
});
378404

379405
if (!message) {
@@ -420,6 +446,7 @@ export class RoomResource extends Resource<Args> {
420446
index,
421447
serializedCardFromFragments: this.serializedCardFromFragments,
422448
events: this.events,
449+
skills: this.skills,
423450
commandResultEvent: event,
424451
});
425452
messageBuilder.updateMessageCommandResult(message);

packages/host/app/services/command-service.ts

+14
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
CommandContextStamp,
1717
ResolvedCodeRef,
1818
isResolvedCodeRef,
19+
getClass,
1920
} from '@cardstack/runtime-common';
2021

2122
import type MatrixService from '@cardstack/host/services/matrix-service';
@@ -130,6 +131,19 @@ export default class CommandService extends Service {
130131

131132
// lookup command
132133
let { command: commandToRun } = this.commands.get(command.name) ?? {};
134+
// If we don't find it in the one-offs, start searching for
135+
// one in the skills we can construct
136+
if (!commandToRun) {
137+
// here we can get the coderef from the messagecommand
138+
let commandCodeRef = command.codeRef;
139+
if (commandCodeRef) {
140+
let CommandConstructor = (await getClass(
141+
commandCodeRef,
142+
this.loaderService.loader,
143+
)) as { new (context: CommandContext): Command<any, any> };
144+
commandToRun = new CommandConstructor(this.commandContext);
145+
}
146+
}
133147

134148
if (commandToRun) {
135149
// Get the input type and validate/construct the payload

packages/host/app/services/matrix-service.ts

+56-2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
LooseCardResource,
2828
ResolvedCodeRef,
2929
aiBotUsername,
30+
getClass,
3031
} from '@cardstack/runtime-common';
3132

3233
import {
@@ -42,6 +43,7 @@ import {
4243
APP_BOXEL_CARD_FORMAT,
4344
APP_BOXEL_CARDFRAGMENT_MSGTYPE,
4445
APP_BOXEL_COMMAND_MSGTYPE,
46+
APP_BOXEL_COMMAND_DEFINITIONS_MSGTYPE,
4547
APP_BOXEL_COMMAND_RESULT_EVENT_TYPE,
4648
APP_BOXEL_COMMAND_RESULT_WITH_NO_OUTPUT_MSGTYPE,
4749
APP_BOXEL_COMMAND_RESULT_WITH_OUTPUT_MSGTYPE,
@@ -73,10 +75,11 @@ import type {
7375
MatrixEvent as DiscreteMatrixEvent,
7476
CommandResultWithNoOutputContent,
7577
CommandResultWithOutputContent,
78+
CommandDefinitionsContent,
7679
} from 'https://cardstack.com/base/matrix-event';
7780

7881
import type { Tool } from 'https://cardstack.com/base/matrix-event';
79-
import { SkillCard } from 'https://cardstack.com/base/skill-card';
82+
import { CommandField, SkillCard } from 'https://cardstack.com/base/skill-card';
8083

8184
import AddSkillsToRoomCommand from '../commands/add-skills-to-room';
8285
import { importResource } from '../resources/import';
@@ -513,7 +516,8 @@ export default class MatrixService extends Service {
513516
| CardMessageContent
514517
| CardFragmentContent
515518
| CommandResultWithNoOutputContent
516-
| CommandResultWithOutputContent,
519+
| CommandResultWithOutputContent
520+
| CommandDefinitionsContent,
517521
) {
518522
let roomData = await this.ensureRoomData(roomId);
519523
return roomData.mutex.dispatch(async () => {
@@ -578,11 +582,61 @@ export default class MatrixService extends Service {
578582
}
579583
}
580584

585+
async addCommandDefinitionsToRoomHistory(
586+
commandDefinitions: CommandField[],
587+
roomId: string,
588+
) {
589+
// Create the command defs so getting the json schema
590+
// and send it to the matrix room.
591+
let commandDefinitionSchemas: {
592+
codeRef: ResolvedCodeRef;
593+
tool: Tool;
594+
}[] = [];
595+
const mappings = await basicMappings(this.loaderService.loader);
596+
for (let commandDef of commandDefinitions) {
597+
const Command = await getClass(
598+
commandDef.codeRef,
599+
this.loaderService.loader,
600+
);
601+
const command = new Command(this.commandService.commandContext);
602+
const name = commandDef.functionName;
603+
commandDefinitionSchemas.push({
604+
codeRef: commandDef.codeRef,
605+
tool: {
606+
type: 'function',
607+
function: {
608+
name,
609+
description: command.description,
610+
parameters: {
611+
type: 'object',
612+
properties: {
613+
description: {
614+
type: 'string',
615+
},
616+
...(await command.getInputJsonSchema(this.cardAPI, mappings)),
617+
},
618+
required: ['attributes', 'description'],
619+
},
620+
},
621+
},
622+
});
623+
}
624+
await this.sendEvent(roomId, 'm.room.message', {
625+
msgtype: APP_BOXEL_COMMAND_DEFINITIONS_MSGTYPE,
626+
body: 'Command Definitions',
627+
data: {
628+
commandDefinitions: commandDefinitionSchemas,
629+
},
630+
});
631+
}
632+
581633
async addSkillCardsToRoomHistory(
582634
skills: SkillCard[],
583635
roomId: string,
584636
opts?: CardAPI.SerializeOpts,
585637
): Promise<string[]> {
638+
const commandDefinitions = skills.flatMap((skill) => skill.commands);
639+
await this.addCommandDefinitionsToRoomHistory(commandDefinitions, roomId);
586640
return this.addCardsToRoom(skills, roomId, this.skillCardHashes, opts);
587641
}
588642

0 commit comments

Comments
 (0)