Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Commands on skill cards #2132

Merged
merged 63 commits into from
Feb 26, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
45951e5
Skills can contain links to commands
IanCal Feb 9, 2025
bc512a3
Add sending of command definitions
IanCal Feb 10, 2025
5d68bc2
Add function name to commands in skills, support in aibot
IanCal Feb 10, 2025
8eb5e5b
Add lookup and loading of the commands on the host side
IanCal Feb 10, 2025
94d149a
Remove logging
IanCal Feb 10, 2025
e1a463e
Improve comment to explain what we're actually doing
IanCal Feb 10, 2025
1e96eb4
Export default and class name
IanCal Feb 10, 2025
4423aed
Undo erroneous capitalisation
IanCal Feb 10, 2025
69d94e3
No default export, keep original casing
IanCal Feb 11, 2025
e0912ca
Fix imports
IanCal Feb 11, 2025
7ab89f8
Fix more named imports
IanCal Feb 11, 2025
d97f4c8
Inconsistent casing
IanCal Feb 11, 2025
c0e3850
Stronger typing for TS
IanCal Feb 11, 2025
3eb3739
Remove unused import
IanCal Feb 11, 2025
4039f66
Add boxel UI state command to shimmed modules and fix import
IanCal Feb 11, 2025
7efc2d6
Fix casing in import
IanCal Feb 11, 2025
e068363
Again, casing issues
IanCal Feb 11, 2025
b6d0297
Test running command from skill
IanCal Feb 11, 2025
63d3519
Test for commands on skills
IanCal Feb 11, 2025
56b2c12
Fix hash suffix
IanCal Feb 11, 2025
b0f66c3
Update hashing function
IanCal Feb 11, 2025
a69a31a
Remove logging
IanCal Feb 11, 2025
a65d474
Avoid creating room resources in multiple places
IanCal Feb 11, 2025
43e109e
Improve setting of active llm
IanCal Feb 11, 2025
7d986f9
Load skills as we load events
IanCal Feb 11, 2025
7fb4bf4
Remove unneccessary async
IanCal Feb 11, 2025
4cbc99a
Support processing more out of order, add members and user ids as qui…
IanCal Feb 12, 2025
fcfd03c
Iterate specifically
IanCal Feb 12, 2025
1f7fe65
test removing restriction on users for tests
IanCal Feb 12, 2025
8a22952
Bring back limits for room resource loading
IanCal Feb 12, 2025
a7ea41d
Remove pauses?
IanCal Feb 13, 2025
54beb97
Merge branch 'main' into return-of-the-skill-commands
IanCal Feb 13, 2025
bd907fa
Fix eslint issues
lukemelia Feb 17, 2025
a85f478
Lint fixes
lukemelia Feb 17, 2025
7df2e0e
Merge branch 'main' into return-of-the-skill-commands
lukemelia Feb 17, 2025
78860a3
Merge branch 'main' into return-of-the-skill-commands
lukemelia Feb 17, 2025
738afb3
Eliminate a relative reference in seed realm that shouldn't be relative
lukemelia Feb 17, 2025
393fe3e
Merge branch 'main' into return-of-the-skill-commands
lukemelia Feb 17, 2025
d01b19a
Better handling of commands exported as default
lukemelia Feb 18, 2025
7544d50
Merge branch 'main' into return-of-the-skill-commands
lukemelia Feb 18, 2025
22ecb4e
Merge branch 'faster-matrix-specs' into return-of-the-skill-commands
lukemelia Feb 18, 2025
5c32d82
Merge branch 'main' into return-of-the-skill-commands
lukemelia Feb 18, 2025
2d3ad9d
Safer checking for toolCall
lukemelia Feb 18, 2025
447be78
Merge branch 'main' into return-of-the-skill-commands
lukemelia Feb 19, 2025
b20fb12
Fix an import in generator code
lukemelia Feb 19, 2025
167b145
Merge branch 'main' into return-of-the-skill-commands
lukemelia Feb 19, 2025
ba864b2
Merge branch 'main' into return-of-the-skill-commands
lukemelia Feb 19, 2025
7be953a
Merge branch 'reuse-room-resource' into return-of-the-skill-commands
lukemelia Feb 19, 2025
8b99982
Merge branch 'main' into return-of-the-skill-commands
lukemelia Feb 19, 2025
bad2fd1
Merge branch 'check-room-members-before-processing-events' into retur…
lukemelia Feb 21, 2025
65b9a47
Merge branch 'main' into return-of-the-skill-commands
lukemelia Feb 21, 2025
5e51d57
Merge branch 'main' into return-of-the-skill-commands
lukemelia Feb 24, 2025
d46fd35
Merge branch 'main' into return-of-the-skill-commands
lukemelia Feb 24, 2025
0182bd5
Remove unneeded changes
lukemelia Feb 24, 2025
a88f906
AI test fixes
lukemelia Feb 24, 2025
0ffa9f1
Merge branch 'code-ref-modules' into return-of-the-skill-commands
lukemelia Feb 24, 2025
57d4693
Merge branch 'main' into return-of-the-skill-commands
lukemelia Feb 24, 2025
3a8219e
Merge branch 'main' into return-of-the-skill-commands
lukemelia Feb 25, 2025
2c43b0e
Merge branch 'main' into return-of-the-skill-commands
lukemelia Feb 25, 2025
af764f2
Merge branch 'main' into return-of-the-skill-commands
lukemelia Feb 25, 2025
519e618
Merge branch 'main' into return-of-the-skill-commands
lukemelia Feb 25, 2025
4309af8
Merge branch 'main' into return-of-the-skill-commands
lukemelia Feb 25, 2025
e218961
Merge branch 'main' into return-of-the-skill-commands
lukemelia Feb 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 44 additions & 2 deletions packages/ai-bot/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@
SkillsConfigEvent,
ActiveLLMEvent,
CommandResultEvent,
CommandDefinitionsEvent,
} from 'https://cardstack.com/base/matrix-event';
import { MatrixEvent, type IRoomEvent } from 'matrix-js-sdk';
import { ChatCompletionMessageToolCall } from 'openai/resources/chat/completions';
import * as Sentry from '@sentry/node';
import { logger } from '@cardstack/runtime-common';
import {
APP_BOXEL_COMMAND_DEFINITIONS_MSGTYPE,
APP_BOXEL_COMMAND_RESULT_EVENT_TYPE,
APP_BOXEL_COMMAND_RESULT_WITH_OUTPUT_MSGTYPE,
} from '../runtime-common/matrix-constants';
Expand All @@ -29,6 +31,7 @@
DEFAULT_LLM,
APP_BOXEL_ACTIVE_LLM,
} from '@cardstack/runtime-common/matrix-constants';
import { SkillCard } from '../base/skill-card';

Check failure on line 34 in packages/ai-bot/helpers.ts

View workflow job for this annotation

GitHub Actions / Lint

'SkillCard' is defined but never used. Allowed unused vars must match /^_/u

let log = logger('ai-bot');

Expand Down Expand Up @@ -86,7 +89,7 @@
cardFragments,
);
let skills = getEnabledSkills(eventList, cardFragments);
let tools = getTools(history, aiBotUserId);
let tools = getTools(history, skills, aiBotUserId);
let toolChoice = getToolChoice(history, aiBotUserId);
let messages = getModifyPrompt(history, aiBotUserId, tools, skills);
let model = getModel(eventList);
Expand Down Expand Up @@ -322,10 +325,46 @@

export function getTools(
history: DiscreteMatrixEvent[],
enabledSkills: LooseCardResource[],
aiBotUserId: string,
): Tool[] {
// Build map directly from messages
let enabledCommandNames = new Set<string>();
let toolMap = new Map<string, Tool>();

// Get the list of all names from enabled skills
for (let skill of enabledSkills) {
if (skill.attributes?.commands) {
let { commands } = skill.attributes;
for (let command of commands) {
enabledCommandNames.add(command.functionName);
}
}
}

// Iterate over the command definitions, and add any tools that are in
// enabled skills to the tool map
let commandDefinitionEvents: CommandDefinitionsEvent[] = history.filter(
(event) =>
event.type === 'm.room.message' &&
event.content.msgtype === APP_BOXEL_COMMAND_DEFINITIONS_MSGTYPE,
) as CommandDefinitionsEvent[];

for (let event of commandDefinitionEvents) {
let { content } = event;
let { commandDefinitions } = content.data;
console.log('commands', commandDefinitions);
for (let commandDefinition of commandDefinitions) {
if (enabledCommandNames.has(commandDefinition.tool.function.name)) {
toolMap.set(
commandDefinition.tool.function.name,
commandDefinition.tool,
);
}
}
}

// Add in tools from the user's messages
for (let event of history) {
if (event.type !== 'm.room.message' || event.sender == aiBotUserId) {
continue;
Expand Down Expand Up @@ -608,7 +647,10 @@
export function eventRequiresResponse(event: MatrixEvent) {
// If it's a message, we should respond unless it's a card fragment
if (event.getType() === 'm.room.message') {
if (event.getContent().msgtype === APP_BOXEL_CARDFRAGMENT_MSGTYPE) {
if (
event.getContent().msgtype === APP_BOXEL_CARDFRAGMENT_MSGTYPE ||
event.getContent().msgtype === APP_BOXEL_COMMAND_DEFINITIONS_MSGTYPE
) {
return false;
}
return true;
Expand Down
26 changes: 26 additions & 0 deletions packages/base/matrix-event.gts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
APP_BOXEL_MESSAGE_MSGTYPE,
APP_BOXEL_ROOM_SKILLS_EVENT_TYPE,
APP_BOXEL_ACTIVE_LLM,
APP_BOXEL_COMMAND_DEFINITIONS_MSGTYPE,
} from '@cardstack/runtime-common/matrix-constants';

interface BaseMatrixEvent {
Expand Down Expand Up @@ -160,6 +161,17 @@ export interface CardMessageEvent extends BaseMatrixEvent {
};
}

export interface CommandDefinitionsEvent extends BaseMatrixEvent {
type: 'm.room.message';
content: CommandDefinitionsContent;
unsigned: {
age: number;
transaction_id: string;
prev_content?: any;
prev_sender?: string;
};
}

export interface Tool {
type: 'function';
function: {
Expand Down Expand Up @@ -246,6 +258,19 @@ export interface CommandResultEvent extends BaseMatrixEvent {
};
}

export interface CommandDefinitionsContent {
msgtype: typeof APP_BOXEL_COMMAND_DEFINITIONS_MSGTYPE;
body: string;
data: {
commandDefinitions: {
codeRef: {
module: string;
name: string;
};
tool: Tool;
}[];
};
}
export interface CommandResultWithOutputContent {
'm.relates_to': {
rel_type: 'm.annotation';
Expand Down Expand Up @@ -276,6 +301,7 @@ export type MatrixEvent =
| MessageEvent
| CommandEvent
| CommandResultEvent
| CommandDefinitionsEvent
| CardMessageEvent
| RoomNameEvent
| RoomTopicEvent
Expand Down
47 changes: 46 additions & 1 deletion packages/base/skill-card.gts
Original file line number Diff line number Diff line change
@@ -1,11 +1,56 @@
import BooleanField from './boolean';
import CodeRefField from './code-ref';
import MarkdownField from './markdown';
import { CardDef, Component, field, contains } from './card-api';
import StringField from './string';
import {
CardDef,
Component,
field,
FieldDef,
contains,
containsMany,
} from './card-api';
import RobotIcon from '@cardstack/boxel-icons/robot';

export class CommandField extends FieldDef {
static displayName = 'CommandField';
@field codeRef = contains(CodeRefField, {
description: 'An absolute code reference to the command to be executed',
});
@field requiresApproval = contains(BooleanField, {
description:
'If true, this command will require human approval before it is executed in the host.',
});

@field functionName = contains(StringField, {
description: 'The name of the function to be executed',
computeVia: function (this: CommandField) {
if (!this.codeRef?.module || !this.codeRef?.name) {
return '';
}

// Simple hash function that works in all environments
const djb2 = (str: string): string => {
let hash = 5381;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash; // Convert to 32-bit integer
}
return Math.abs(hash).toString(16).slice(0, 4);
};

const input = `${this.codeRef.module}#${this.codeRef.name}`;
return `${this.codeRef.name}_${djb2(input)}`;
},
});
}

export class SkillCard extends CardDef {
static displayName = 'Skill';
static icon = RobotIcon;
@field instructions = contains(MarkdownField);
@field commands = containsMany(CommandField);
static embedded = class Embedded extends Component<typeof this> {
<template>
<@fields.title />
Expand Down
15 changes: 15 additions & 0 deletions packages/host/app/lib/matrix-classes/message-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,21 @@ export default class MessageCommand {
}
}

async getCommandCodeRef() {
let roomResource = this.matrixService.roomResources.get(
this.message.roomId,
);
if (!roomResource) {
return undefined;
}
// We need to wait for the skills to actually be loaded
await roomResource.waitForAllSkills();
let commandDefinition = roomResource.commands.find(
(command) => command.functionName === this.name,
);
return commandDefinition?.codeRef;
}

async getCommandResultCard(): Promise<CardDef | undefined> {
let cardDoc = this.commandResultCardDoc;
if (!cardDoc) {
Expand Down
45 changes: 43 additions & 2 deletions packages/host/app/resources/room.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { type LooseSingleCardDocument } from '@cardstack/runtime-common';
import {
APP_BOXEL_CARDFRAGMENT_MSGTYPE,
APP_BOXEL_COMMAND_MSGTYPE,
APP_BOXEL_COMMAND_DEFINITIONS_MSGTYPE,
APP_BOXEL_COMMAND_RESULT_EVENT_TYPE,
DEFAULT_LLM,
} from '@cardstack/runtime-common/matrix-constants';
Expand All @@ -28,6 +29,7 @@ import type {
CardMessageEvent,
MessageEvent,
CommandResultEvent,
CommandDefinitionsEvent,
} from 'https://cardstack.com/base/matrix-event';

import { SkillCard } from 'https://cardstack.com/base/skill-card';
Expand All @@ -51,6 +53,7 @@ interface SkillId {
skillCardId: string;
skillEventId: string;
isActive: boolean;
loadingPromise?: Promise<void>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ef4 was counseling against resources exposing promises -- let's talk this through.

}

interface Args {
Expand Down Expand Up @@ -167,8 +170,9 @@ export class RoomResource extends Resource<Args> {
continue;
}
let cardId = cardDoc.data.id;
let loadingPromise;
if (!this._skillCardsCache.has(cardId)) {
this.cardService
loadingPromise = this.cardService
.createFromSerialized(cardDoc.data, cardDoc)
.then((skillsCard) => {
this._skillCardsCache.set(cardId, skillsCard as SkillCard);
Expand All @@ -178,6 +182,7 @@ export class RoomResource extends Resource<Args> {
skillCardId: cardDoc.data.id,
skillEventId: eventId,
isActive: skillsConfig.enabledEventIds.includes(eventId),
loadingPromise,
});
}
return result;
Expand All @@ -199,6 +204,24 @@ export class RoomResource extends Resource<Args> {
.filter(Boolean) as Skill[];
}

async waitForAllSkills() {
let skillCardLoadingPromises = this.skillIds
.map(({ loadingPromise }) => loadingPromise)
.filter(Boolean);
await Promise.all(skillCardLoadingPromises);
}

get commands() {
// get all the skills
let commands = [];
for (let skill of this.skills) {
if (skill.isActive) {
commands.push(...skill.card.commands);
}
}
return commands;
}

get roomId() {
return this._previousRoomId;
}
Expand Down Expand Up @@ -288,19 +311,37 @@ export class RoomResource extends Resource<Args> {
});
}

private isCommandDefinitionsEvent(
event:
| MessageEvent
| CommandEvent
| CardMessageEvent
| CommandDefinitionsEvent,
): event is CommandDefinitionsEvent {
return event.content.msgtype === APP_BOXEL_COMMAND_DEFINITIONS_MSGTYPE;
}

private loadRoomMessage({
roomId,
event,
index,
}: {
roomId: string;
event: MessageEvent | CommandEvent | CardMessageEvent;
event:
| MessageEvent
| CommandEvent
| CardMessageEvent
| CommandDefinitionsEvent;
index: number;
}) {
if (event.content.msgtype === APP_BOXEL_CARDFRAGMENT_MSGTYPE) {
this._fragmentCache.set(event.event_id, event.content);
return;
}
if (this.isCommandDefinitionsEvent(event)) {
// We don't want to show this to the user
return;
}

this.upsertMessage({ roomId, event, index });
}
Expand Down
14 changes: 14 additions & 0 deletions packages/host/app/services/command-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
CommandContextStamp,
ResolvedCodeRef,
isResolvedCodeRef,
getClass,
} from '@cardstack/runtime-common';

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

// lookup command
let { command: commandToRun } = this.commands.get(command.name) ?? {};
// If we don't find it in the one-offs, start searching for
// one in the skills we can construct
if (!commandToRun) {
// here we can get the coderef from the messagecommand
let commandCodeRef = await command.getCommandCodeRef();
if (commandCodeRef) {
let CommandConstructor = (await getClass(
commandCodeRef,
this.loaderService.loader,
)) as { new (context: CommandContext): Command<any, any> };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this type should get a name?

commandToRun = new CommandConstructor(this.commandContext);
}
}

if (commandToRun) {
// Get the input type and validate/construct the payload
Expand Down
Loading
Loading