@cosmoops/connect-sdk 0.29.0 → 0.30.1
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/CHANGELOG.md +99 -0
- package/dist/cjs/client/components/ask-ai-history.js +106 -0
- package/dist/cjs/client/components/ask-ai-turns.js +87 -0
- package/dist/cjs/client/components/ask-ai.js +155 -30
- package/dist/cjs/client/provider.js +2 -2
- package/dist/cjs/server/client.js +1 -1
- package/dist/cjs/server/handler.js +75 -0
- package/dist/cjs/server/resources/ai.js +55 -0
- package/dist/esm/client/components/ask-ai-history.js +103 -0
- package/dist/esm/client/components/ask-ai-turns.js +82 -0
- package/dist/esm/client/components/ask-ai.js +122 -30
- package/dist/esm/client/provider.js +2 -2
- package/dist/esm/server/client.js +1 -1
- package/dist/esm/server/handler.js +75 -0
- package/dist/esm/server/resources/ai.js +55 -0
- package/dist/types/client/components/ask-ai-history.d.ts +25 -0
- package/dist/types/client/components/ask-ai-turns.d.ts +55 -0
- package/dist/types/client/components/ask-ai.d.ts +1 -1
- package/dist/types/client/provider.d.ts +8 -1
- package/dist/types/server/client.d.ts +1 -1
- package/dist/types/server/resources/ai.d.ts +49 -1
- package/dist/types/types.d.ts +48 -0
- package/package.json +1 -1
|
@@ -388,6 +388,12 @@ export function createConnectHandler(options) {
|
|
|
388
388
|
// decision, so there is nothing for this layer to check more strictly.
|
|
389
389
|
screenContext: body.screenContext,
|
|
390
390
|
screens: body.screens,
|
|
391
|
+
// The browser's own id for this conversation, so the turn is stored
|
|
392
|
+
// and can be reopened. Safe to relay: it decides *which* of the
|
|
393
|
+
// caller's own conversations is written to, never whose - Connect
|
|
394
|
+
// refuses an id belonging to another user or another application, and
|
|
395
|
+
// the owner comes from `asUser` below.
|
|
396
|
+
...(typeof body.conversationId === "string" ? { conversationId: body.conversationId } : {}),
|
|
391
397
|
// Connect re-derives the workspace from this token, so the line above
|
|
392
398
|
// is belt to its braces - the browser cannot reach another tenant's
|
|
393
399
|
// Context Index even if this route were changed to trust the request.
|
|
@@ -406,6 +412,75 @@ export function createConnectHandler(options) {
|
|
|
406
412
|
},
|
|
407
413
|
});
|
|
408
414
|
},
|
|
415
|
+
/**
|
|
416
|
+
* Ask AI history - the list, one transcript, and a deletion.
|
|
417
|
+
*
|
|
418
|
+
* The same rule as every other route: the user comes from the caller's own
|
|
419
|
+
* session, forwarded as `asUser`, and Connect reads both the person and
|
|
420
|
+
* the workspace from it. Nothing here takes an owner from the request, so
|
|
421
|
+
* a browser cannot page through anybody else's conversations - which
|
|
422
|
+
* matters more here than on most routes, because a transcript is the most
|
|
423
|
+
* candid record this API holds of what somebody was trying to do.
|
|
424
|
+
*
|
|
425
|
+
* The id is the only thing a browser supplies. It selects among the
|
|
426
|
+
* caller's own conversations and nothing more: an id belonging to another
|
|
427
|
+
* user is a 404, and one belonging to another application the same, so a
|
|
428
|
+
* shared workspace's two products cannot read each other's.
|
|
429
|
+
*
|
|
430
|
+
* It travels as `?id=` rather than as a path segment, and the store routes
|
|
431
|
+
* are why. This table is keyed by an exact `METHOD /path` string; the only
|
|
432
|
+
* thing that matches a *prefix* is `isStoreRoute`, kept to a closed list
|
|
433
|
+
* precisely so the bridge cannot drift into a generic proxy. Widening that
|
|
434
|
+
* matcher for one more resource would be the first step of exactly that
|
|
435
|
+
* drift, so the id goes in the query and the route keys stay exact.
|
|
436
|
+
*
|
|
437
|
+
* `requireToken` rather than `requireWorkspace`: Connect resolves the
|
|
438
|
+
* workspace from the token itself and refuses a staff session there, so
|
|
439
|
+
* the extra identity round trip would buy a differently-worded 403 and
|
|
440
|
+
* nothing else.
|
|
441
|
+
*/
|
|
442
|
+
"GET /ai/conversations": async (request) => {
|
|
443
|
+
const url = new URL(request.url);
|
|
444
|
+
const limit = url.searchParams.get("limit");
|
|
445
|
+
const cursor = url.searchParams.get("cursor");
|
|
446
|
+
return json(await client.ai.listConversations({
|
|
447
|
+
asUser: await requireToken(request),
|
|
448
|
+
// Forwarded only when present, so Connect's own defaults apply
|
|
449
|
+
// rather than this layer inventing a second set of them.
|
|
450
|
+
...(limit !== null ? { limit: Number(limit) } : {}),
|
|
451
|
+
...(cursor ? { cursor } : {}),
|
|
452
|
+
}));
|
|
453
|
+
},
|
|
454
|
+
"GET /ai/conversation": async (request) => {
|
|
455
|
+
const conversationId = new URL(request.url).searchParams.get("id");
|
|
456
|
+
if (!conversationId) {
|
|
457
|
+
throw new ConnectError({
|
|
458
|
+
status: 400,
|
|
459
|
+
code: "validation_error",
|
|
460
|
+
message: "id is required",
|
|
461
|
+
endpoint: "GET /ai/conversation",
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
return json(await client.ai.getConversation({
|
|
465
|
+
conversationId,
|
|
466
|
+
asUser: await requireToken(request),
|
|
467
|
+
}));
|
|
468
|
+
},
|
|
469
|
+
"DELETE /ai/conversation": async (request) => {
|
|
470
|
+
const conversationId = new URL(request.url).searchParams.get("id");
|
|
471
|
+
if (!conversationId) {
|
|
472
|
+
throw new ConnectError({
|
|
473
|
+
status: 400,
|
|
474
|
+
code: "validation_error",
|
|
475
|
+
message: "id is required",
|
|
476
|
+
endpoint: "DELETE /ai/conversation",
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
return json(await client.ai.deleteConversation({
|
|
480
|
+
conversationId,
|
|
481
|
+
asUser: await requireToken(request),
|
|
482
|
+
}));
|
|
483
|
+
},
|
|
409
484
|
/**
|
|
410
485
|
* The store, forwarded.
|
|
411
486
|
*
|
|
@@ -30,6 +30,10 @@ export class AiResource {
|
|
|
30
30
|
json: {
|
|
31
31
|
workspaceId: input.workspaceId,
|
|
32
32
|
messages: input.messages,
|
|
33
|
+
// Omitted when absent, which is how a caller opts out of history: no
|
|
34
|
+
// id, nothing stored. Connect also stores nothing for a call carrying
|
|
35
|
+
// no `asUser`, because a conversation belongs to a person.
|
|
36
|
+
...(input.conversationId ? { conversationId: input.conversationId } : {}),
|
|
33
37
|
// Omitted entirely when absent, rather than sent as `undefined` - an
|
|
34
38
|
// application that describes no screens should send no field.
|
|
35
39
|
...(input.screens ? { screens: input.screens } : {}),
|
|
@@ -40,6 +44,57 @@ export class AiResource {
|
|
|
40
44
|
signal: options.signal,
|
|
41
45
|
});
|
|
42
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* The signed-in user's past conversations in your application, most recent
|
|
49
|
+
* first.
|
|
50
|
+
*
|
|
51
|
+
* `asUser` is required rather than optional here, unlike on `chat`: the rows
|
|
52
|
+
* are scoped to a person, and there is no application-wide view of them to
|
|
53
|
+
* fall back to. Connect reads the user and the workspace from the token.
|
|
54
|
+
*
|
|
55
|
+
* Cursor-paginated with no total - the list is ordered by last use, which
|
|
56
|
+
* moves while it is being read.
|
|
57
|
+
*/
|
|
58
|
+
listConversations(input) {
|
|
59
|
+
const query = new URLSearchParams();
|
|
60
|
+
if (input.limit !== undefined)
|
|
61
|
+
query.set("limit", String(input.limit));
|
|
62
|
+
if (input.cursor)
|
|
63
|
+
query.set("cursor", input.cursor);
|
|
64
|
+
const suffix = query.size > 0 ? `?${query.toString()}` : "";
|
|
65
|
+
return request(this.transport, {
|
|
66
|
+
method: "GET",
|
|
67
|
+
path: `/ai/conversations${suffix}`,
|
|
68
|
+
headers: actingAs(input.asUser),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* One stored transcript, oldest turn first.
|
|
73
|
+
*
|
|
74
|
+
* `turns[].parts` comes back exactly as the model produced it - prose,
|
|
75
|
+
* reasoning and tool calls in order - so it can be handed straight back to a
|
|
76
|
+
* chat UI. Not this user's, or not your application's, is a `404` either way.
|
|
77
|
+
*/
|
|
78
|
+
getConversation(input) {
|
|
79
|
+
return request(this.transport, {
|
|
80
|
+
method: "GET",
|
|
81
|
+
path: `/ai/conversations/${encodeURIComponent(input.conversationId)}`,
|
|
82
|
+
headers: actingAs(input.asUser),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Deletes a conversation and every turn beneath it.
|
|
87
|
+
*
|
|
88
|
+
* Idempotent - one already gone answers `{ deleted: true }`, so a retry after
|
|
89
|
+
* a dropped response does not read as a failure.
|
|
90
|
+
*/
|
|
91
|
+
deleteConversation(input) {
|
|
92
|
+
return request(this.transport, {
|
|
93
|
+
method: "DELETE",
|
|
94
|
+
path: `/ai/conversations/${encodeURIComponent(input.conversationId)}`,
|
|
95
|
+
headers: actingAs(input.asUser),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
43
98
|
/** Extract structured JSON from a file or image URL, against a platform extraction schema referenced by id. */
|
|
44
99
|
async extract(input) {
|
|
45
100
|
const result = await request(this.transport, {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { AiConversationDetail } from "../../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Past Ask AI conversations, for reopening or deleting.
|
|
4
|
+
*
|
|
5
|
+
* ## It fetches only while it is open
|
|
6
|
+
*
|
|
7
|
+
* `<ConnectAskAi>` is mounted for the whole application shell, so an
|
|
8
|
+
* unconditional read here would put a request on every page load to populate a
|
|
9
|
+
* list most people never open. It reads on open, and again whenever the panel
|
|
10
|
+
* has written a new conversation since - which is what `refreshKey` is for.
|
|
11
|
+
*
|
|
12
|
+
* ## Deletion confirms inline, not in a dialog
|
|
13
|
+
*
|
|
14
|
+
* Destructive actions confirm; this one does it at the weight it deserves.
|
|
15
|
+
* Losing a transcript is recoverable by asking again, not a cancelled
|
|
16
|
+
* subscription, and a modal over a 400px panel would be heavier than the thing
|
|
17
|
+
* it guards. The row swaps to "Delete? / Cancel" and stays there until
|
|
18
|
+
* answered.
|
|
19
|
+
*/
|
|
20
|
+
export declare function AskAiHistory({ refreshKey, activeConversationId, onOpenConversation, }: Readonly<{
|
|
21
|
+
/** Changes when the panel has written something, so the list re-reads. */
|
|
22
|
+
refreshKey: number;
|
|
23
|
+
activeConversationId: string;
|
|
24
|
+
onOpenConversation: (id: string, detail: AiConversationDetail) => void;
|
|
25
|
+
}>): import("react").JSX.Element;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { AiConversationTurn } from "../../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* One piece of an assistant turn, in the order it arrived.
|
|
4
|
+
*
|
|
5
|
+
* Blocks rather than one text string, because an answer interleaves prose and
|
|
6
|
+
* drawings - a sentence, a table, the sentence that reads the table - and
|
|
7
|
+
* flattening it would silently reorder the model's own argument.
|
|
8
|
+
*/
|
|
9
|
+
export type AskAiBlock = {
|
|
10
|
+
kind: "text";
|
|
11
|
+
text: string;
|
|
12
|
+
} | {
|
|
13
|
+
kind: "tool";
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
input: unknown | null;
|
|
17
|
+
};
|
|
18
|
+
export interface AskAiTurn {
|
|
19
|
+
id: string;
|
|
20
|
+
role: "user" | "assistant";
|
|
21
|
+
blocks: AskAiBlock[];
|
|
22
|
+
/** The model's streamed thoughts, if it sent any. */
|
|
23
|
+
reasoning: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Index of the final text block, or `-1`. A reverse loop rather than
|
|
27
|
+
* `findLastIndex`, which is ES2023 - this package targets ES2022 so its
|
|
28
|
+
* published output stays usable on the runtimes its `engines` field claims.
|
|
29
|
+
*/
|
|
30
|
+
export declare function lastTextBlockIndex(blocks: AskAiBlock[]): number;
|
|
31
|
+
/** Appends a text delta to the open text block, or opens one after a widget. */
|
|
32
|
+
export declare function appendText(blocks: AskAiBlock[], text: string): AskAiBlock[];
|
|
33
|
+
/**
|
|
34
|
+
* Rebuilds the panel's turns from a stored transcript.
|
|
35
|
+
*
|
|
36
|
+
* The inverse of what the stream reader does live, and it has to agree with it
|
|
37
|
+
* or a reopened conversation renders differently from the one the user just
|
|
38
|
+
* had. Two shapes, one source: the stream delivers `text-delta`,
|
|
39
|
+
* `reasoning-delta` and `tool-input-available` frames, and storage keeps the
|
|
40
|
+
* settled `UIMessage` parts those frames built - `{ type: "text" }`,
|
|
41
|
+
* `{ type: "reasoning" }`, and `{ type: "tool-<name>" }` carrying the input the
|
|
42
|
+
* widget was drawn from.
|
|
43
|
+
*
|
|
44
|
+
* Three rules, each of which is a thing that would otherwise render wrong:
|
|
45
|
+
*
|
|
46
|
+
* - **A tool part with no input is dropped, not restored.** `input: null` means
|
|
47
|
+
* "still arriving" to the renderer, which draws a skeleton - correct live,
|
|
48
|
+
* and a shimmer that never resolves on a transcript that finished days ago.
|
|
49
|
+
* - **Reasoning is hoisted per turn**, not left in place. It is rendered above
|
|
50
|
+
* the answer in a disclosure, which is where it sat while streaming.
|
|
51
|
+
* - **A part this build cannot place is skipped rather than guessed at.**
|
|
52
|
+
* `parts` is the AI SDK's own shape and gains kinds over time; an unknown one
|
|
53
|
+
* costs a widget, and inventing a rendering for it costs the whole turn.
|
|
54
|
+
*/
|
|
55
|
+
export declare function turnsFromStored(turns: readonly AiConversationTurn[]): AskAiTurn[];
|
|
@@ -99,8 +99,15 @@ export interface ConnectContextValue {
|
|
|
99
99
|
* `path` through `useConnectResource` still needs to call that hook's own
|
|
100
100
|
* `refetch()` afterward; clearing the cache entry alone does not re-render
|
|
101
101
|
* anything by itself.
|
|
102
|
+
*
|
|
103
|
+
* `method` widens this to the one other verb the bridge answers, `DELETE`,
|
|
104
|
+
* for the routes where removal is the whole operation and a body would be
|
|
105
|
+
* ceremony - Ask AI's history is the first. It stays a two-value union
|
|
106
|
+
* rather than `string`: everything else on this bridge is a POST, and a
|
|
107
|
+
* free-form verb here would be an invitation to invent REST semantics the
|
|
108
|
+
* bridge's exact-match route table does not have.
|
|
102
109
|
*/
|
|
103
|
-
mutateResource<T>(path: string, body?: unknown): Promise<T>;
|
|
110
|
+
mutateResource<T>(path: string, body?: unknown, method?: "POST" | "DELETE"): Promise<T>;
|
|
104
111
|
/**
|
|
105
112
|
* Drops one path's cached read so the next `fetchResource` for it goes to
|
|
106
113
|
* the bridge again.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type TransportOptions } from "../http.js";
|
|
2
|
-
import type { ChatMessage, ConnectScreen } from "../../types.js";
|
|
2
|
+
import type { AiConversationDetail, AiConversationList, ChatMessage, ConnectScreen } from "../../types.js";
|
|
3
3
|
/**
|
|
4
4
|
* RAG chat and structured extraction against a workspace's data.
|
|
5
5
|
*
|
|
@@ -21,6 +21,15 @@ export declare class AiResource {
|
|
|
21
21
|
workspaceId: string;
|
|
22
22
|
messages: ChatMessage[];
|
|
23
23
|
asUser?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Your own id for this conversation, sent with every turn of it.
|
|
26
|
+
*
|
|
27
|
+
* What makes the conversation *resumable*: Connect upserts against it and
|
|
28
|
+
* `conversations.list`/`get` read it back. Persisted only alongside
|
|
29
|
+
* `asUser` - a conversation belongs to a person, and a server-to-server
|
|
30
|
+
* call acts as nobody. Omit it to keep a turn out of history entirely.
|
|
31
|
+
*/
|
|
32
|
+
conversationId?: string;
|
|
24
33
|
/**
|
|
25
34
|
* Your application's screens, so the assistant answers in your product's
|
|
26
35
|
* words and can link into it - see `ConnectScreen`. Prose and links only:
|
|
@@ -34,6 +43,45 @@ export declare class AiResource {
|
|
|
34
43
|
}, options?: {
|
|
35
44
|
signal?: AbortSignal;
|
|
36
45
|
}): Promise<Response>;
|
|
46
|
+
/**
|
|
47
|
+
* The signed-in user's past conversations in your application, most recent
|
|
48
|
+
* first.
|
|
49
|
+
*
|
|
50
|
+
* `asUser` is required rather than optional here, unlike on `chat`: the rows
|
|
51
|
+
* are scoped to a person, and there is no application-wide view of them to
|
|
52
|
+
* fall back to. Connect reads the user and the workspace from the token.
|
|
53
|
+
*
|
|
54
|
+
* Cursor-paginated with no total - the list is ordered by last use, which
|
|
55
|
+
* moves while it is being read.
|
|
56
|
+
*/
|
|
57
|
+
listConversations(input: {
|
|
58
|
+
asUser: string;
|
|
59
|
+
limit?: number;
|
|
60
|
+
cursor?: string;
|
|
61
|
+
}): Promise<AiConversationList>;
|
|
62
|
+
/**
|
|
63
|
+
* One stored transcript, oldest turn first.
|
|
64
|
+
*
|
|
65
|
+
* `turns[].parts` comes back exactly as the model produced it - prose,
|
|
66
|
+
* reasoning and tool calls in order - so it can be handed straight back to a
|
|
67
|
+
* chat UI. Not this user's, or not your application's, is a `404` either way.
|
|
68
|
+
*/
|
|
69
|
+
getConversation(input: {
|
|
70
|
+
conversationId: string;
|
|
71
|
+
asUser: string;
|
|
72
|
+
}): Promise<AiConversationDetail>;
|
|
73
|
+
/**
|
|
74
|
+
* Deletes a conversation and every turn beneath it.
|
|
75
|
+
*
|
|
76
|
+
* Idempotent - one already gone answers `{ deleted: true }`, so a retry after
|
|
77
|
+
* a dropped response does not read as a failure.
|
|
78
|
+
*/
|
|
79
|
+
deleteConversation(input: {
|
|
80
|
+
conversationId: string;
|
|
81
|
+
asUser: string;
|
|
82
|
+
}): Promise<{
|
|
83
|
+
deleted: true;
|
|
84
|
+
}>;
|
|
37
85
|
/** Extract structured JSON from a file or image URL, against a platform extraction schema referenced by id. */
|
|
38
86
|
extract<T = unknown>(input: {
|
|
39
87
|
schemaId: string;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -538,6 +538,54 @@ export interface ChatMessage {
|
|
|
538
538
|
role: "user" | "assistant" | "system";
|
|
539
539
|
[field: string]: unknown;
|
|
540
540
|
}
|
|
541
|
+
/**
|
|
542
|
+
* One row of Ask AI history.
|
|
543
|
+
*
|
|
544
|
+
* A conversation exists only if the turns that made it carried a
|
|
545
|
+
* `conversationId` *and* were sent on a user's behalf - see `AiResource.chat`.
|
|
546
|
+
* It belongs to one person in one of your workspaces, and never leaves your
|
|
547
|
+
* application: a workspace subscribing to two products keeps two lists.
|
|
548
|
+
*/
|
|
549
|
+
export interface AiConversation {
|
|
550
|
+
id: string;
|
|
551
|
+
/** Derived from the first thing the user said - no model call, so it costs nothing. */
|
|
552
|
+
title: string;
|
|
553
|
+
/** ISO-8601. */
|
|
554
|
+
createdAt: string;
|
|
555
|
+
/**
|
|
556
|
+
* ISO-8601, and the sort key - a conversation returned to a week later is at
|
|
557
|
+
* the top, not where it was created.
|
|
558
|
+
*/
|
|
559
|
+
updatedAt: string;
|
|
560
|
+
}
|
|
561
|
+
export interface AiConversationList {
|
|
562
|
+
conversations: AiConversation[];
|
|
563
|
+
/** Pass back as `cursor` for the next page; `null` at the end. */
|
|
564
|
+
nextCursor: string | null;
|
|
565
|
+
}
|
|
566
|
+
/** One stored turn, in the AI SDK's own shape. */
|
|
567
|
+
export interface AiConversationTurn {
|
|
568
|
+
id: string;
|
|
569
|
+
role: "user" | "assistant";
|
|
570
|
+
/**
|
|
571
|
+
* The message's parts exactly as the model produced them - prose, reasoning
|
|
572
|
+
* and tool calls, in the order they arrived.
|
|
573
|
+
*
|
|
574
|
+
* Opaque on purpose. The shape is the AI SDK's and moves with it, so a type
|
|
575
|
+
* restating it here would start rejecting transcripts the moment the SDK adds
|
|
576
|
+
* a part kind. Flattening it to text is the other mistake: the order is the
|
|
577
|
+
* model's own argument, and a flattened turn cannot be resumed.
|
|
578
|
+
*/
|
|
579
|
+
parts: unknown[];
|
|
580
|
+
/** Position in the conversation. Turns arrive in this order. */
|
|
581
|
+
sequence: number;
|
|
582
|
+
/** ISO-8601. */
|
|
583
|
+
createdAt: string;
|
|
584
|
+
}
|
|
585
|
+
export interface AiConversationDetail {
|
|
586
|
+
conversation: AiConversation;
|
|
587
|
+
turns: AiConversationTurn[];
|
|
588
|
+
}
|
|
541
589
|
export interface WorkspaceApp {
|
|
542
590
|
applicationId: string;
|
|
543
591
|
/** Stable, human-readable key - branch on this, not `applicationId`, which is per-environment. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmoops/connect-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.1",
|
|
4
4
|
"description": "Official server-side SDK for the CosmoOps Connect platform API - Login with Connect, entitlements, billing and metering.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"type": "commonjs",
|