@vellumai/assistant 0.12.2-dev.202609211115.1844039 → 0.12.2-dev.202609211515.9bea579
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/openapi.yaml +56 -0
- package/package.json +1 -1
- package/src/__tests__/document-create-route.test.ts +88 -0
- package/src/__tests__/document-sync-tags.test.ts +27 -0
- package/src/documents/document-store.ts +23 -0
- package/src/runtime/routes/documents-routes.ts +51 -0
- package/src/tools/document/document-tool.ts +11 -12
package/openapi.yaml
CHANGED
|
@@ -12949,6 +12949,62 @@ paths:
|
|
|
12949
12949
|
schema:
|
|
12950
12950
|
type: string
|
|
12951
12951
|
format: binary
|
|
12952
|
+
/v1/documents/create:
|
|
12953
|
+
post:
|
|
12954
|
+
operationId: documents_create_post
|
|
12955
|
+
summary: Create a document
|
|
12956
|
+
description: Create an empty document owned by a conversation and return it with its new surface ID.
|
|
12957
|
+
tags:
|
|
12958
|
+
- documents
|
|
12959
|
+
requestBody:
|
|
12960
|
+
required: true
|
|
12961
|
+
content:
|
|
12962
|
+
application/json:
|
|
12963
|
+
schema:
|
|
12964
|
+
type: object
|
|
12965
|
+
properties:
|
|
12966
|
+
conversationId:
|
|
12967
|
+
type: string
|
|
12968
|
+
description: Owning conversation
|
|
12969
|
+
title:
|
|
12970
|
+
description: Document title. Defaults to "Untitled Document".
|
|
12971
|
+
type: string
|
|
12972
|
+
required:
|
|
12973
|
+
- conversationId
|
|
12974
|
+
responses:
|
|
12975
|
+
"200":
|
|
12976
|
+
description: Successful response
|
|
12977
|
+
content:
|
|
12978
|
+
application/json:
|
|
12979
|
+
schema:
|
|
12980
|
+
type: object
|
|
12981
|
+
properties:
|
|
12982
|
+
success:
|
|
12983
|
+
type: boolean
|
|
12984
|
+
surfaceId:
|
|
12985
|
+
type: string
|
|
12986
|
+
conversationId:
|
|
12987
|
+
type: string
|
|
12988
|
+
title:
|
|
12989
|
+
type: string
|
|
12990
|
+
content:
|
|
12991
|
+
type: string
|
|
12992
|
+
wordCount:
|
|
12993
|
+
type: number
|
|
12994
|
+
createdAt:
|
|
12995
|
+
type: number
|
|
12996
|
+
updatedAt:
|
|
12997
|
+
type: number
|
|
12998
|
+
required:
|
|
12999
|
+
- success
|
|
13000
|
+
- surfaceId
|
|
13001
|
+
- conversationId
|
|
13002
|
+
- title
|
|
13003
|
+
- content
|
|
13004
|
+
- wordCount
|
|
13005
|
+
- createdAt
|
|
13006
|
+
- updatedAt
|
|
13007
|
+
additionalProperties: false
|
|
12952
13008
|
/v1/domain/register:
|
|
12953
13009
|
post:
|
|
12954
13010
|
operationId: domain_register_post
|
package/package.json
CHANGED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { afterAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_DOCUMENT_TITLE,
|
|
5
|
+
getDocumentsForConversation,
|
|
6
|
+
} from "../documents/document-store.js";
|
|
7
|
+
import { createConversation } from "../persistence/conversation-crud.js";
|
|
8
|
+
import { getDb } from "../persistence/db-connection.js";
|
|
9
|
+
import { initializeDb } from "../persistence/db-init.js";
|
|
10
|
+
import { ROUTES as DOCUMENT_ROUTES } from "../runtime/routes/documents-routes.js";
|
|
11
|
+
import type { RouteDefinition } from "../runtime/routes/types.js";
|
|
12
|
+
import { resetDbForTesting } from "./db-test-helpers.js";
|
|
13
|
+
|
|
14
|
+
await initializeDb();
|
|
15
|
+
|
|
16
|
+
const CONVERSATION_ID = "conv-doc-create";
|
|
17
|
+
|
|
18
|
+
async function create(body: Record<string, unknown>): Promise<unknown> {
|
|
19
|
+
const route = DOCUMENT_ROUTES.find(
|
|
20
|
+
(r: RouteDefinition) => r.operationId === "createDocument",
|
|
21
|
+
)!;
|
|
22
|
+
return await route.handler({ body });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
const db = getDb();
|
|
27
|
+
db.run("DELETE FROM document_conversations");
|
|
28
|
+
db.run("DELETE FROM documents");
|
|
29
|
+
db.run("DELETE FROM conversations");
|
|
30
|
+
createConversation({ id: CONVERSATION_ID });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterAll(() => {
|
|
34
|
+
resetDbForTesting();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("createDocument route", () => {
|
|
38
|
+
test("creates an empty document under a new surface ID and returns it", async () => {
|
|
39
|
+
const result = await create({
|
|
40
|
+
conversationId: CONVERSATION_ID,
|
|
41
|
+
title: "Trip plan",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
expect(result).toMatchObject({
|
|
45
|
+
success: true,
|
|
46
|
+
conversationId: CONVERSATION_ID,
|
|
47
|
+
title: "Trip plan",
|
|
48
|
+
content: "",
|
|
49
|
+
wordCount: 0,
|
|
50
|
+
});
|
|
51
|
+
const { surfaceId } = result as { surfaceId: string };
|
|
52
|
+
expect(surfaceId).toMatch(/^doc-[0-9a-f-]{36}$/);
|
|
53
|
+
expect(
|
|
54
|
+
getDocumentsForConversation(CONVERSATION_ID).map((d) => d.surfaceId),
|
|
55
|
+
).toEqual([surfaceId]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("a missing or blank title falls back to the default", async () => {
|
|
59
|
+
await expect(
|
|
60
|
+
create({ conversationId: CONVERSATION_ID }),
|
|
61
|
+
).resolves.toMatchObject({ title: DEFAULT_DOCUMENT_TITLE });
|
|
62
|
+
await expect(
|
|
63
|
+
create({ conversationId: CONVERSATION_ID, title: " " }),
|
|
64
|
+
).resolves.toMatchObject({ title: DEFAULT_DOCUMENT_TITLE });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("each create mints a distinct document", async () => {
|
|
68
|
+
const first = (await create({ conversationId: CONVERSATION_ID })) as {
|
|
69
|
+
surfaceId: string;
|
|
70
|
+
};
|
|
71
|
+
const second = (await create({ conversationId: CONVERSATION_ID })) as {
|
|
72
|
+
surfaceId: string;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
expect(first.surfaceId).not.toBe(second.surfaceId);
|
|
76
|
+
expect(getDocumentsForConversation(CONVERSATION_ID)).toHaveLength(2);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("rejects a request without a conversation", async () => {
|
|
80
|
+
await expect(create({})).rejects.toThrow(/conversationId is required/);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("rejects a conversation that does not exist", async () => {
|
|
84
|
+
await expect(create({ conversationId: "conv-missing" })).rejects.toThrow(
|
|
85
|
+
/Conversation not found/,
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -203,6 +203,22 @@ describe("document write routes publish documents:list", () => {
|
|
|
203
203
|
});
|
|
204
204
|
});
|
|
205
205
|
|
|
206
|
+
test("creating a document publishes with the caller's client id", async () => {
|
|
207
|
+
const events = await captureSyncEvents(() =>
|
|
208
|
+
invoke("createDocument", {
|
|
209
|
+
body: { conversationId: CONVERSATION_ID },
|
|
210
|
+
headers: { "x-vellum-client-id": "client-abc" },
|
|
211
|
+
}),
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
expect(events).toHaveLength(1);
|
|
215
|
+
expect(events[0].message).toMatchObject({
|
|
216
|
+
type: "sync_changed",
|
|
217
|
+
tags: [SYNC_TAGS.documentsList],
|
|
218
|
+
originClientId: "client-abc",
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
206
222
|
test("linking a document to a conversation publishes", async () => {
|
|
207
223
|
await saveViaRoute();
|
|
208
224
|
|
|
@@ -251,6 +267,17 @@ describe("document routes that change nothing the list shows stay silent", () =>
|
|
|
251
267
|
expect(events).toEqual([]);
|
|
252
268
|
});
|
|
253
269
|
|
|
270
|
+
test("a create for a conversation that does not exist publishes nothing", async () => {
|
|
271
|
+
const events = await captureSyncEvents(async () => {
|
|
272
|
+
await expect(
|
|
273
|
+
invoke("createDocument", {
|
|
274
|
+
body: { conversationId: MISSING_CONVERSATION_ID },
|
|
275
|
+
}),
|
|
276
|
+
).rejects.toThrow(/Conversation not found/);
|
|
277
|
+
});
|
|
278
|
+
expect(events).toEqual([]);
|
|
279
|
+
});
|
|
280
|
+
|
|
254
281
|
test("a link to a document that does not exist publishes nothing", async () => {
|
|
255
282
|
const events = await captureSyncEvents(async () => {
|
|
256
283
|
await expect(
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* background jobs (e.g. proactive artifact generation) can persist documents
|
|
6
6
|
* without going through the HTTP layer.
|
|
7
7
|
*/
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
9
|
+
|
|
8
10
|
import { rawAll, rawGet, rawRun } from "../persistence/raw-query.js";
|
|
9
11
|
import { getLogger } from "../util/logger.js";
|
|
10
12
|
|
|
@@ -456,6 +458,27 @@ export function saveDocument(params: {
|
|
|
456
458
|
}
|
|
457
459
|
}
|
|
458
460
|
|
|
461
|
+
export const DEFAULT_DOCUMENT_TITLE = "Untitled Document";
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Create a new document owned by `conversationId` under a freshly minted
|
|
465
|
+
* surface ID. A missing or empty title falls back to `DEFAULT_DOCUMENT_TITLE`.
|
|
466
|
+
*/
|
|
467
|
+
export function createDocument(params: {
|
|
468
|
+
conversationId: string;
|
|
469
|
+
title?: string;
|
|
470
|
+
content?: string;
|
|
471
|
+
}): ReturnType<typeof saveDocument> {
|
|
472
|
+
const content = params.content ?? "";
|
|
473
|
+
return saveDocument({
|
|
474
|
+
surfaceId: `doc-${randomUUID()}`,
|
|
475
|
+
conversationId: params.conversationId,
|
|
476
|
+
title: params.title || DEFAULT_DOCUMENT_TITLE,
|
|
477
|
+
content,
|
|
478
|
+
wordCount: countWords(content),
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
459
482
|
// ---------------------------------------------------------------------------
|
|
460
483
|
// Find-and-replace
|
|
461
484
|
// ---------------------------------------------------------------------------
|
|
@@ -8,10 +8,13 @@ import { z } from "zod";
|
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
10
|
addDocumentConversation,
|
|
11
|
+
createDocument,
|
|
12
|
+
DEFAULT_DOCUMENT_TITLE,
|
|
11
13
|
getDocumentById,
|
|
12
14
|
getDocumentsForConversation,
|
|
13
15
|
saveDocument,
|
|
14
16
|
} from "../../documents/document-store.js";
|
|
17
|
+
import { getConversation } from "../../persistence/conversation-crud.js";
|
|
15
18
|
import { rawAll } from "../../persistence/raw-query.js";
|
|
16
19
|
import { getLogger } from "../../util/logger.js";
|
|
17
20
|
import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
|
|
@@ -214,6 +217,54 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
214
217
|
},
|
|
215
218
|
},
|
|
216
219
|
|
|
220
|
+
{
|
|
221
|
+
operationId: "createDocument",
|
|
222
|
+
endpoint: "documents/create",
|
|
223
|
+
method: "POST",
|
|
224
|
+
policy: {
|
|
225
|
+
requiredScopes: ["settings.write"],
|
|
226
|
+
allowedPrincipalTypes: ACTOR_PRINCIPALS,
|
|
227
|
+
},
|
|
228
|
+
summary: "Create a document",
|
|
229
|
+
description:
|
|
230
|
+
"Create an empty document owned by a conversation and return it with its new surface ID.",
|
|
231
|
+
tags: ["documents"],
|
|
232
|
+
requestBody: z.object({
|
|
233
|
+
conversationId: z.string().describe("Owning conversation"),
|
|
234
|
+
title: z
|
|
235
|
+
.string()
|
|
236
|
+
.optional()
|
|
237
|
+
.describe(`Document title. Defaults to "${DEFAULT_DOCUMENT_TITLE}".`),
|
|
238
|
+
}),
|
|
239
|
+
responseBody: documentPayloadSchema,
|
|
240
|
+
handler: ({ body, headers }) => {
|
|
241
|
+
const { conversationId, title } = (body ?? {}) as {
|
|
242
|
+
conversationId?: string;
|
|
243
|
+
title?: string;
|
|
244
|
+
};
|
|
245
|
+
if (!conversationId || typeof conversationId !== "string") {
|
|
246
|
+
throw new BadRequestError("conversationId is required");
|
|
247
|
+
}
|
|
248
|
+
if (title !== undefined && typeof title !== "string") {
|
|
249
|
+
throw new BadRequestError("title must be a string");
|
|
250
|
+
}
|
|
251
|
+
if (!getConversation(conversationId)) {
|
|
252
|
+
throw new NotFoundError("Conversation not found");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const result = createDocument({ conversationId, title: title?.trim() });
|
|
256
|
+
if (!result.success) {
|
|
257
|
+
throw new InternalError(result.error);
|
|
258
|
+
}
|
|
259
|
+
const doc = getDocumentById(result.surfaceId);
|
|
260
|
+
if (!doc) {
|
|
261
|
+
throw new InternalError("Created document could not be read back");
|
|
262
|
+
}
|
|
263
|
+
publishDocumentsChanged(getOriginClientId(headers));
|
|
264
|
+
return { success: true, ...doc };
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
|
|
217
268
|
{
|
|
218
269
|
operationId: "linkDocumentConversation",
|
|
219
270
|
endpoint: "documents/:id/conversations",
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
|
|
3
1
|
import { z } from "zod";
|
|
4
2
|
|
|
5
3
|
import {
|
|
6
4
|
addDocumentConversation,
|
|
5
|
+
createDocument,
|
|
6
|
+
DEFAULT_DOCUMENT_TITLE,
|
|
7
7
|
deleteDocument,
|
|
8
8
|
findInDocument,
|
|
9
9
|
findRecentEmptyDocumentByTitle,
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
getDocumentsForConversation,
|
|
12
12
|
isDocumentAssociatedWithConversation,
|
|
13
13
|
replaceInDocument,
|
|
14
|
-
saveDocument,
|
|
15
14
|
searchDocumentsByTitle,
|
|
16
15
|
updateDocumentContent,
|
|
17
16
|
} from "../../documents/document-store.js";
|
|
@@ -283,7 +282,7 @@ export function executeDocumentCreate(
|
|
|
283
282
|
return invalidToolInputResult("document_create", parsedInput.error);
|
|
284
283
|
}
|
|
285
284
|
throwIfCancelled(context);
|
|
286
|
-
const title = parsedInput.data.title ||
|
|
285
|
+
const title = parsedInput.data.title || DEFAULT_DOCUMENT_TITLE;
|
|
287
286
|
const initialContent = parsedInput.data.initial_content || "";
|
|
288
287
|
|
|
289
288
|
const reused = maybeReuseEmptyDocument(title, initialContent, context);
|
|
@@ -291,21 +290,21 @@ export function executeDocumentCreate(
|
|
|
291
290
|
return reused;
|
|
292
291
|
}
|
|
293
292
|
|
|
294
|
-
const surfaceId = `doc-${randomUUID()}`;
|
|
295
|
-
|
|
296
293
|
// Persist the document so any client (web or macOS) can fetch it via
|
|
297
294
|
// GET /v1/documents/:id. The macOS client may later update the row
|
|
298
295
|
// via document_save; ON CONFLICT DO UPDATE handles that.
|
|
299
|
-
const
|
|
300
|
-
.split(/\s+/)
|
|
301
|
-
.filter((w) => w.length > 0).length;
|
|
302
|
-
saveDocument({
|
|
303
|
-
surfaceId,
|
|
296
|
+
const created = createDocument({
|
|
304
297
|
conversationId: context.conversationId,
|
|
305
298
|
title,
|
|
306
299
|
content: initialContent,
|
|
307
|
-
wordCount,
|
|
308
300
|
});
|
|
301
|
+
if (!created.success) {
|
|
302
|
+
return {
|
|
303
|
+
content: JSON.stringify({ success: false, error: created.error }),
|
|
304
|
+
isError: true,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
const { surfaceId } = created;
|
|
309
308
|
|
|
310
309
|
// Send document_editor_show message to open the built-in RTE
|
|
311
310
|
if (context.sendToClient) {
|