Skip to content

Commit 91bbe05

Browse files
committed
Merge branch 'main' into cs-7989-display-attached-file-in-message-bubble
2 parents 7b2b39e + 22ebcca commit 91bbe05

File tree

141 files changed

+3110
-2348
lines changed

Some content is hidden

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

141 files changed

+3110
-2348
lines changed

Diff for: docs/README.md

+7-8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
# Documentation about Boxel
32

43
## Prerequisites
@@ -9,13 +8,13 @@ It also has a feature to save an .svg image, which be opened in Chrome for a dec
98

109
## Concepts
1110

12-
The following are important concepts:
11+
The following are important concepts:
1312

14-
- [Card and Field Definition Relationships](card-def-field-def-relationships.md): There is a subtle distinction between card and fields to consider when creating cards.
15-
- [Inheritance](card-inheritance.md): Cards can be extended based upon user's custom needs -- no reinventing the wheel.
16-
- [Rendering](card-rendering.md): Cards can be rendered easily in the browser. Each card renders differently based upon how it is related and what context it exists in.
13+
- [Card and Field Definition Relationships](card-def-field-def-relationships.md): There is a subtle distinction between card and fields to consider when creating cards.
14+
- [Inheritance](card-inheritance.md): Cards can be extended based upon user's custom needs -- no reinventing the wheel.
15+
- [Rendering](card-rendering.md): Cards can be rendered easily in the browser. Each card renders differently based upon how it is related and what context it exists in.
1716
- [Serialization and Deserialization](card-serialization-deserialization.md): Cards have to be adapted to a consistent JSON format before being sent over-the-wire to other consumers.
18-
- [Computed Fields](computed-fields.md): Computed fields work too! We can compute on the data that is already contained in a card to build more complex logic.
19-
- [Indexing](indexing.md): Indexing powers the re-rendering of cards when it's dependencies get updated.
17+
- [Computed Fields](computed-fields.md): Computed fields work too! We can compute on the data that is already contained in a card to build more complex logic.
18+
- [Indexing](indexing.md): Indexing powers the re-rendering of cards when it's dependencies get updated.
2019
- [Realm](realm.md): Realms are storage for cards that have their own underlying permissions and indexer.
21-
- [Search](search.md): Every Card is searchable within and across realms.
20+
- [Search](search.md): Every Card is searchable within and across realms.

Diff for: docs/queue.md

+10-6
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,33 @@
11
Our queue system is postgres based job queue system (resurrected from hub v2). This leverages postgres pub/sub capabilities to create a job queue system that implements our `Queue` interface (we previously created a mock queue that implements this same interface for browser testing).
22

3-
The queue is controlled by the `jobs` table. We can monitor and control our queue using this table. The following is an example query of this table after running our tests.
3+
The queue is controlled by the `jobs` table. We can monitor and control our queue using this table. The following is an example query of this table after running our tests.
44

55
```
66
> SELECT * FROM JOBS;
7-
8-
id | category | args | status | created_at | finished_at | queue | result
7+
8+
id | category | args | status | created_at | finished_at | queue | result
99
----+-----------+------+----------+----------------------------+----------------------------+-----------------+--------
1010
1 | increment | 17 | resolved | 2024-04-19 16:57:43.305961 | 2024-04-19 16:57:43.311274 | increment-queue | 18
1111
```
1212

1313
On system start up we can register job handlers whose responsibility it is to run queued jobs (these handlers can horizontally scale if we so choose). A handler registration looks like this:
14+
1415
```ts
1516
queue.register('increment', async (a: number) => a + 1);
1617
```
17-
This is a real simple example that just adds 1 to the job's input arguments. A handler ran return an async result as JSONB value which is stored in the `jobs.result` column of the `jobs` table. This handler defines a "category" called `increment` for this function that it has registered. A handler processes the queue by looking for the oldest job that isn't running and handles that first.
18+
19+
This is a real simple example that just adds 1 to the job's input arguments. A handler ran return an async result as JSONB value which is stored in the `jobs.result` column of the `jobs` table. This handler defines a "category" called `increment` for this function that it has registered. A handler processes the queue by looking for the oldest job that isn't running and handles that first.
1820

1921
Clients of the queue that wish to run jobs can do so by specifying the category of job that they wish to run, the queue that they wish to use, and input arguments for the job (the input arguments can be a JSONB value which is stored in the `jobs.args` column of the `jobs` table).
22+
2023
```ts
2124
let job = await queue.publish<number>('increment', 17, {
2225
queueName: 'increment-queue',
2326
});
2427
```
28+
2529
The caller is handed a `job` object. This object has an `id` property and a `done` property that returns a promise for the job's return value (which is a parameterized type) when the job is completed. Note that the `queueName` is optional. If no name is supplied then the queue name `"default"` is used. The `queueName` is used to control job concurrency. Jobs are processed in each `queueName` serially.
2630

27-
When a job is first published to a queue it is assigned a status of `unfulfilled`. When a job has completed successfully it is assigned a status of `resolved`. If a job throws an error it is assigned a status of `rejected` and the error is serialized in the `jobs.result` column.
31+
When a job is first published to a queue it is assigned a status of `unfulfilled`. When a job has completed successfully it is assigned a status of `resolved`. If a job throws an error it is assigned a status of `rejected` and the error is serialized in the `jobs.result` column.
2832

29-
Using SQL you can monitor the progress of the jobs in the queue, as well as, you can manipulate the results of the queue processing by setting `jobs.status`, `jobs.result`, and `jobs.queueName` using SQL.
33+
Using SQL you can monitor the progress of the jobs in the queue, as well as, you can manipulate the results of the queue processing by setting `jobs.status`, `jobs.result`, and `jobs.queueName` using SQL.

Diff for: package.json

+2-4
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,11 @@
2727
"@embroider/util": "1.13.1",
2828
"@glimmer/tracking>@glimmer/validator": "0.84.3",
2929
"jsesc": "^3.0.0",
30-
"ember-modifier": "^4.1.0",
31-
"prettier": "github:cardstack/prettier#glimmer-style-tag-in-template-support"
30+
"ember-modifier": "^4.1.0"
3231
},
3332
"peerDependencyRules": {
3433
"allowedVersions": {
3534
"mustache": "3",
36-
"prettier@github:cardstack/prettier#glimmer-style-tag-in-template-support": "3.1.0-dev",
3735
"ember-qunit@5.1.2>ember-source": "*"
3836
}
3937
},
@@ -70,7 +68,7 @@
7068
"eslint-plugin-prefer-let": "^3.0.1",
7169
"eslint-plugin-prettier": "^5.0.0",
7270
"hcl2-parser": "^1.0.3",
73-
"prettier": "^2.7.1",
71+
"prettier": "^3.5.1",
7472
"prettier-plugin-ember-template-tag": "^1.1.0",
7573
"typescript": "~5.1.6"
7674
},

Diff for: packages/ai-bot/helpers.ts

+123-13
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,10 @@ export class HistoryConstructionError extends Error {
7676
}
7777
}
7878

79-
export function getPromptParts(
79+
export async function getPromptParts(
8080
eventList: DiscreteMatrixEvent[],
8181
aiBotUserId: string,
82-
): PromptParts {
82+
): Promise<PromptParts> {
8383
let cardFragments: Map<string, CardFragmentContent> =
8484
extractCardFragmentsFromEvents(eventList);
8585
let history: DiscreteMatrixEvent[] = constructHistory(
@@ -89,7 +89,7 @@ export function getPromptParts(
8989
let skills = getEnabledSkills(eventList, cardFragments);
9090
let tools = getTools(history, aiBotUserId);
9191
let toolChoice = getToolChoice(history, aiBotUserId);
92-
let messages = getModifyPrompt(history, aiBotUserId, tools, skills);
92+
let messages = await getModifyPrompt(history, aiBotUserId, tools, skills);
9393
let model = getModel(eventList);
9494
return {
9595
tools,
@@ -321,6 +321,113 @@ export function getRelevantCards(
321321
};
322322
}
323323

324+
export async function loadCurrentlyAttachedFiles(
325+
history: DiscreteMatrixEvent[],
326+
aiBotUserId: string,
327+
): Promise<
328+
{
329+
url: string;
330+
name: string;
331+
contentType?: string;
332+
content: string | undefined;
333+
error: string | undefined;
334+
}[]
335+
> {
336+
let lastMessageEventByUser = history.findLast(
337+
(event) => event.sender !== aiBotUserId,
338+
);
339+
340+
let mostRecentUserMessageContent = lastMessageEventByUser?.content as {
341+
msgtype?: string;
342+
data?: {
343+
attachedFiles?: { url: string; name: string; contentType?: string }[];
344+
};
345+
};
346+
347+
if (
348+
!mostRecentUserMessageContent ||
349+
mostRecentUserMessageContent.msgtype !== APP_BOXEL_MESSAGE_MSGTYPE
350+
) {
351+
return [];
352+
}
353+
354+
// We are only interested in downloading the most recently attached files -
355+
// downloading older ones is not needed since the prompt that is being constructed
356+
// should operate on fresh data
357+
if (!mostRecentUserMessageContent.data?.attachedFiles?.length) {
358+
return [];
359+
}
360+
361+
let attachedFiles = mostRecentUserMessageContent.data.attachedFiles;
362+
363+
return Promise.all(
364+
attachedFiles.map(
365+
async (attachedFile: {
366+
url: string;
367+
name: string;
368+
contentType?: string;
369+
}) => {
370+
try {
371+
let content: string | undefined;
372+
let error: string | undefined;
373+
if (attachedFile.contentType?.startsWith('text/')) {
374+
let response = await (globalThis as any).fetch(attachedFile.url);
375+
if (!response.ok) {
376+
throw new Error(`HTTP error. Status: ${response.status}`);
377+
}
378+
content = await response.text();
379+
} else {
380+
error = `Unsupported file type: ${attachedFile.contentType}. For now, only text files are supported.`;
381+
}
382+
383+
return {
384+
url: attachedFile.url,
385+
name: attachedFile.name,
386+
contentType: attachedFile.contentType,
387+
content,
388+
error,
389+
};
390+
} catch (error) {
391+
log.error(`Failed to fetch file ${attachedFile.url}:`, error);
392+
Sentry.captureException(error, {
393+
extra: { fileUrl: attachedFile.url, fileName: attachedFile.name },
394+
});
395+
return {
396+
url: attachedFile.url,
397+
name: attachedFile.name,
398+
contentType: attachedFile.contentType,
399+
content: undefined,
400+
error: `Error loading attached file: ${(error as Error).message}`,
401+
};
402+
}
403+
},
404+
),
405+
);
406+
}
407+
408+
export function attachedFilesToPrompt(
409+
attachedFiles: {
410+
url: string;
411+
name: string;
412+
contentType?: string;
413+
content: string | undefined;
414+
error: string | undefined;
415+
}[],
416+
): string {
417+
if (!attachedFiles.length) {
418+
return 'No attached files';
419+
}
420+
return attachedFiles
421+
.map((f) => {
422+
if (f.error) {
423+
return `${f.name}: ${f.error}`;
424+
}
425+
426+
return `${f.name}: ${f.content}`;
427+
})
428+
.join('\n');
429+
}
430+
324431
export function getTools(
325432
history: DiscreteMatrixEvent[],
326433
aiBotUserId: string,
@@ -431,7 +538,7 @@ function toPromptMessageWithToolResult(
431538
};
432539
}
433540

434-
export function getModifyPrompt(
541+
export async function getModifyPrompt(
435542
history: DiscreteMatrixEvent[],
436543
aiBotUserId: string,
437544
tools: Tool[] = [],
@@ -501,15 +608,18 @@ export function getModifyPrompt(
501608
history,
502609
aiBotUserId,
503610
);
504-
let systemMessage =
505-
MODIFY_SYSTEM_MESSAGE +
506-
`
507-
The user currently has given you the following data to work with:
508-
Cards:\n`;
509-
systemMessage += attachedCardsToMessage(
510-
mostRecentlyAttachedCard,
511-
attachedCards,
512-
);
611+
612+
let attachedFiles = await loadCurrentlyAttachedFiles(history, aiBotUserId);
613+
614+
let systemMessage = `${MODIFY_SYSTEM_MESSAGE}
615+
The user currently has given you the following data to work with:
616+
617+
Cards: ${attachedCardsToMessage(mostRecentlyAttachedCard, attachedCards)}
618+
619+
Attached files:
620+
${attachedFilesToPrompt(attachedFiles)}
621+
`;
622+
513623
if (skillCards.length) {
514624
systemMessage += SKILL_INSTRUCTIONS_MESSAGE;
515625
systemMessage += skillCardsToMessage(skillCards);

Diff for: packages/ai-bot/main.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ Common issues are:
214214
let eventList = (initial!.messages?.chunk ||
215215
[]) as DiscreteMatrixEvent[];
216216
try {
217-
promptParts = getPromptParts(eventList, aiBotUserId);
217+
promptParts = await getPromptParts(eventList, aiBotUserId);
218218
} catch (e) {
219219
log.error(e);
220220
responder.finalize(

0 commit comments

Comments
 (0)