Skip to content

Commit

Permalink
Merge branch 'main' into cs-6438-check-authorization-of-user-on-each-…
Browse files Browse the repository at this point in the history
…api-request
  • Loading branch information
jurgenwerk committed Jan 30, 2024
2 parents 5c12c81 + 24ead8b commit 96484b7
Show file tree
Hide file tree
Showing 22 changed files with 944 additions and 209 deletions.
15 changes: 12 additions & 3 deletions packages/ai-bot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,26 @@ Once logged in, create a room and invite the aibot - it should join automaticall

It will be able to see any cards shared in the chat and can respond using GPT4 if you ask for content modifications (as a start, try 'can you create some sample data for this?'). The response should stream back and give you several options, these get applied as patches to the shared card if it is in your stack.

You can deliberately trigger a specific patch by sending a message that starts `debugpatch:` and has the JSON patch you want returned. For example:
### Debugging

You can deliberately trigger a specific patch by sending a message that starts `debug:patch:` and has the JSON patch you want returned. For example:

```
debugpatch:{"firstName": "David"}
debug:patch:{"firstName": "David"}
```

This will return a patch with the ID of the last card you uploaded. This does not hit GPT4 and is useful for testing the integration of the two components without waiting for streaming responses.

You can set a room name with `debug:title:set:`

```
debug:title:set:My Room
```

And you can trigger room naming with `debug:title:create` on its own.

## Testing

### Unit tests

Run `pnpm test`

63 changes: 63 additions & 0 deletions packages/ai-bot/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,69 @@ export function getFunctions(history: IRoomEvent[], aiBotUserId: string) {
}
}

export function shouldSetRoomTitle(
rawEventLog: IRoomEvent[],
aiBotUserId: string,
additionalCommands = 0, // These are any that have been sent since the event log was retrieved
) {
// If the room title has been set already, we don't want to set it again
let nameEvents = rawEventLog.filter((event) => event.type === 'm.room.name');
if (nameEvents.length > 1) {
return false;
}

// If there has been a command sent,
// we should be at a stage where we can set the room title
let commandsSent = rawEventLog.filter(
(event) => event.content.msgtype === 'org.boxel.command',
);

if (commandsSent.length + additionalCommands > 0) {
return true;
}

// If there has been a 5 user messages we should still set the room title
let userEvents = rawEventLog.filter(
(event) => event.sender !== aiBotUserId && event.type === 'm.room.message',
);
if (userEvents.length >= 5) {
return true;
}

return false;
}

export function getStartOfConversation(
history: IRoomEvent[],
aiBotUserId: string,
maxLength = 2000,
) {
/**
* Get just the start of the conversation
* useful for summarising while limiting the context
*/
let messages: OpenAIPromptMessage[] = [];
let totalLength = 0;
for (let event of history) {
let body = event.content.body;
if (body && totalLength + body.length <= maxLength) {
if (event.sender === aiBotUserId) {
messages.push({
role: 'assistant',
content: body,
});
} else {
messages.push({
role: 'user',
content: body,
});
}
totalLength += body.length;
}
}
return messages;
}

export function getModifyPrompt(
history: IRoomEvent[],
aiBotUserId: string,
Expand Down
141 changes: 106 additions & 35 deletions packages/ai-bot/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import {
getModifyPrompt,
cleanContent,
getFunctions,
getStartOfConversation,
shouldSetRoomTitle,
} from './helpers';
import { OpenAIError } from 'openai/error';

Expand Down Expand Up @@ -165,6 +167,89 @@ async function sendError(
}
}

async function setTitle(
client: MatrixClient,
room: Room,
history: IRoomEvent[],
userId: string,
) {
let startOfConversation = [
{
role: 'system',
content: `You are a chat titling system, you must read the conversation and return a suggested title of no more than six words.
Do NOT say talk or discussion or discussing or chat or chatting, this is implied by the context.
Explain the general actions and user intent.`,
},
...getStartOfConversation(history, userId),
];
startOfConversation.push({
role: 'user',
content: 'Create a short title for this chat, limited to 6 words.',
});
try {
let result = await openai.chat.completions.create(
{
model: 'gpt-3.5-turbo-1106',
messages: startOfConversation,
stream: false,
},
{
maxRetries: 5,
},
);
let title = result.choices[0].message.content || 'no title';
// strip leading and trailing quotes
title = title.replace(/^"(.*)"$/, '$1');
log.info('Setting room title to', title);
return await client.setRoomName(room.roomId, title);
} catch (error) {
return await sendError(client, room, error, undefined);
}
}

async function handleDebugCommands(
eventBody: string,
client: MatrixClient,
room: Room,
history: IRoomEvent[],
userId: string,
) {
// Explicitly set the room name
if (eventBody.startsWith('debug:title:set:')) {
return await client.setRoomName(
room.roomId,
eventBody.split('debug:title:set:')[1],
);
}
// Use GPT to set the room title
else if (eventBody.startsWith('debug:title:create')) {
return await setTitle(client, room, history, userId);
} else if (eventBody.startsWith('debug:patch:')) {
let patchMessage = eventBody.split('debug:patch:')[1];
// If there's a card attached, we need to split it off to parse the json
patchMessage = patchMessage.split('(Card')[0];
let attributes = {};
try {
attributes = JSON.parse(patchMessage);
} catch (error) {
await sendMessage(
client,
room,
'Error parsing your debug patch as JSON: ' + patchMessage,
undefined,
);
}
let command = {
type: 'patch',
id: getLastUploadedCardID(history),
patch: {
attributes: attributes,
},
};
return await sendOption(client, room, command);
}
}

(async () => {
const matrixUrl = process.env.MATRIX_URL || 'http://localhost:8008';
let client = createClient({
Expand Down Expand Up @@ -193,9 +278,9 @@ Common issues are:
`);
process.exit(1);
});
let { user_id: userId } = auth;
let { user_id: aiBotUserId } = auth;
client.on(RoomMemberEvent.Membership, function (_event, member) {
if (member.membership === 'invite' && member.userId === userId) {
if (member.membership === 'invite' && member.userId === aiBotUserId) {
client
.joinRoom(member.roomId)
.then(function () {
Expand All @@ -214,15 +299,11 @@ Common issues are:
client.on(
RoomEvent.Timeline,
async function (event, room, toStartOfTimeline) {
let eventBody = event.getContent().body;
if (!room) {
return;
}
log.info(
'(%s) %s :: %s',
room?.name,
event.getSender(),
event.getContent().body,
);
log.info('(%s) %s :: %s', room?.name, event.getSender(), eventBody);

if (event.event.origin_server_ts! < startTime) {
return;
Expand All @@ -233,7 +314,7 @@ Common issues are:
if (event.getType() !== 'm.room.message') {
return; // only print messages
}
if (event.getSender() === userId) {
if (event.getSender() === aiBotUserId) {
return;
}
let initialMessage: ISendEventResponse = await client.sendHtmlMessage(
Expand All @@ -250,35 +331,20 @@ Common issues are:
let history: IRoomEvent[] = constructHistory(eventList);
log.info("Compressed into just the history that's ", history.length);

// While developing the frontend it can be handy to skip GPT and just return some data
if (event.getContent().body.startsWith('debugpatch:')) {
let body = event.getContent().body;
let patchMessage = body.split('debugpatch:')[1];
// If there's a card attached, we need to split it off to parse the json
patchMessage = patchMessage.split('(Card')[0];
let attributes = {};
try {
attributes = JSON.parse(patchMessage);
} catch (error) {
await sendMessage(
client,
room,
'Error parsing your debug patch as JSON: ' + patchMessage,
initialMessage.event_id,
);
}
let command = {
type: 'patch',
id: getLastUploadedCardID(history),
patch: {
attributes: attributes,
},
};
return await sendOption(client, room, command, initialMessage.event_id);
// To assist debugging, handle explicit commands
if (eventBody.startsWith('debug:')) {
return await handleDebugCommands(
eventBody,
client,
room,
history,
aiBotUserId,
);
}

let unsent = 0;
const runner = getResponse(history, userId)
let sentCommands = 0;
const runner = getResponse(history, aiBotUserId)
.on('content', async (_delta, snapshot) => {
unsent += 1;
if (unsent > 5) {
Expand All @@ -305,6 +371,7 @@ Common issues are:
);
}
if (functionCall.name === 'patchCard') {
sentCommands += 1;
return await sendOption(
client,
room,
Expand All @@ -328,6 +395,10 @@ Common issues are:
if (finalContent) {
await sendMessage(client, room, finalContent, initialMessage.event_id);
}

if (shouldSetRoomTitle(eventList, aiBotUserId, sentCommands)) {
return await setTitle(client, room, history, aiBotUserId);
}
return;
},
);
Expand Down
Loading

0 comments on commit 96484b7

Please sign in to comment.