@arcote.tech/arc-chat 0.7.12 → 0.7.13
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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcote.tech/arc-chat",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.13",
|
|
5
5
|
"private": false,
|
|
6
6
|
"description": "Chat module with AI integration for Arc framework",
|
|
7
7
|
"main": "./src/index.ts",
|
|
@@ -10,12 +10,12 @@
|
|
|
10
10
|
"type-check": "tsc --noEmit"
|
|
11
11
|
},
|
|
12
12
|
"peerDependencies": {
|
|
13
|
-
"@arcote.tech/arc": "^0.7.
|
|
14
|
-
"@arcote.tech/arc-ai": "^0.7.
|
|
15
|
-
"@arcote.tech/arc-ai-voice": "^0.7.
|
|
16
|
-
"@arcote.tech/arc-auth": "^0.7.
|
|
17
|
-
"@arcote.tech/arc-ds": "^0.7.
|
|
18
|
-
"@arcote.tech/platform": "^0.7.
|
|
13
|
+
"@arcote.tech/arc": "^0.7.13",
|
|
14
|
+
"@arcote.tech/arc-ai": "^0.7.13",
|
|
15
|
+
"@arcote.tech/arc-ai-voice": "^0.7.13",
|
|
16
|
+
"@arcote.tech/arc-auth": "^0.7.13",
|
|
17
|
+
"@arcote.tech/arc-ds": "^0.7.13",
|
|
18
|
+
"@arcote.tech/platform": "^0.7.13",
|
|
19
19
|
"lucide-react": ">=0.400.0",
|
|
20
20
|
"react": ">=18.0.0",
|
|
21
21
|
"typescript": "^5.0.0"
|
|
@@ -100,6 +100,13 @@ export const createMessageAggregate = <
|
|
|
100
100
|
* otwiera SSE i streaming jest visible od pierwszego chunka.
|
|
101
101
|
*/
|
|
102
102
|
assistantMessageId: messageId.optional(),
|
|
103
|
+
/**
|
|
104
|
+
* JSON-encoded `string[]` z fileId'ami arc-files attachmentów do tej
|
|
105
|
+
* konkretnej wiadomości. Listener AI generation ładuje `ArcFile`
|
|
106
|
+
* records i przekazuje je w `CompletionRequest.files`. Nie zapisujemy
|
|
107
|
+
* w aggregate fields — flow jest read-once przez listener z event.
|
|
108
|
+
*/
|
|
109
|
+
attachmentsJson: string().optional(),
|
|
103
110
|
},
|
|
104
111
|
async (ctx, event) => {
|
|
105
112
|
const p = event.payload;
|
|
@@ -253,6 +260,12 @@ export const createMessageAggregate = <
|
|
|
253
260
|
scopeId,
|
|
254
261
|
content: string().minLength(1),
|
|
255
262
|
model: string(),
|
|
263
|
+
/**
|
|
264
|
+
* Opcjonalna JSON-encoded `string[]` z fileId'ami arc-files
|
|
265
|
+
* dołączonymi do user message. Listener AI generation ładuje
|
|
266
|
+
* pełne records po stronie serwera.
|
|
267
|
+
*/
|
|
268
|
+
attachmentsJson: string().optional(),
|
|
256
269
|
}).handle(
|
|
257
270
|
ONLY_SERVER &&
|
|
258
271
|
(async (ctx, params) => {
|
|
@@ -282,6 +295,7 @@ export const createMessageAggregate = <
|
|
|
282
295
|
content: params.content,
|
|
283
296
|
model: params.model,
|
|
284
297
|
assistantMessageId: assistantMsgId,
|
|
298
|
+
attachmentsJson: params.attachmentsJson,
|
|
285
299
|
});
|
|
286
300
|
|
|
287
301
|
return { messageId: userMsgId, sessionId, assistantMessageId: assistantMsgId };
|
package/src/chat-builder.ts
CHANGED
|
@@ -85,6 +85,17 @@ export interface ArcChatData {
|
|
|
85
85
|
toolChoice: "auto" | "required" | { type: "function"; name: string };
|
|
86
86
|
alias: string | null;
|
|
87
87
|
billTo: BillToFn | null;
|
|
88
|
+
/**
|
|
89
|
+
* `arc-files` aggregate dla file attachmentów (PDF/DOCX/...) do user
|
|
90
|
+
* messages. Gdy ustawione, listener AI generation:
|
|
91
|
+
* - ładuje `ArcFile` records po fileId'ach z `attachmentsJson` user msg,
|
|
92
|
+
* - przekazuje je w `CompletionRequest.files`,
|
|
93
|
+
* - po complete cachuje `boundProviderFiles` w aggregate'cie (lazy
|
|
94
|
+
* upload do OpenAI Files API → file_id zapisany w `providerFileIdsJson`).
|
|
95
|
+
*
|
|
96
|
+
* Consumer wstrzykuje przez `.attachFiles({ File })`.
|
|
97
|
+
*/
|
|
98
|
+
attachFiles: { File: any } | null;
|
|
88
99
|
}
|
|
89
100
|
|
|
90
101
|
const defaultChatData = {
|
|
@@ -101,6 +112,7 @@ const defaultChatData = {
|
|
|
101
112
|
toolChoice: "auto" as const,
|
|
102
113
|
alias: null,
|
|
103
114
|
billTo: null,
|
|
115
|
+
attachFiles: null,
|
|
104
116
|
} as const satisfies ArcChatData;
|
|
105
117
|
|
|
106
118
|
type DefaultChatData = typeof defaultChatData;
|
|
@@ -237,6 +249,22 @@ export class ArcChat<const Data extends ArcChatData = DefaultChatData> {
|
|
|
237
249
|
} as any);
|
|
238
250
|
}
|
|
239
251
|
|
|
252
|
+
/**
|
|
253
|
+
* Włącza obsługę file attachments dla user messages — listener AI
|
|
254
|
+
* generation będzie ładował `ArcFile` records, przekazywał je w
|
|
255
|
+
* `CompletionRequest.files`, i cachował lazy-uploadowane `file_id`'y
|
|
256
|
+
* provider'ów (OpenAI Files API).
|
|
257
|
+
*
|
|
258
|
+
* const files = arcFiles({ name: "ndt", scopeId: workspaceId, ... });
|
|
259
|
+
* chat("identity").attachFiles({ File: files.File }).build();
|
|
260
|
+
*/
|
|
261
|
+
attachFiles(config: { File: any }): ArcChat<Data> {
|
|
262
|
+
return new ArcChat<Data>({
|
|
263
|
+
...this.data,
|
|
264
|
+
attachFiles: config,
|
|
265
|
+
} as any);
|
|
266
|
+
}
|
|
267
|
+
|
|
240
268
|
build() {
|
|
241
269
|
const {
|
|
242
270
|
name,
|
|
@@ -252,6 +280,7 @@ export class ArcChat<const Data extends ArcChatData = DefaultChatData> {
|
|
|
252
280
|
toolChoice,
|
|
253
281
|
alias: aliasOverride,
|
|
254
282
|
billTo,
|
|
283
|
+
attachFiles,
|
|
255
284
|
} = this.data;
|
|
256
285
|
|
|
257
286
|
if (!name) throw new Error("ArcChat: name is required");
|
|
@@ -319,6 +348,54 @@ export class ArcChat<const Data extends ArcChatData = DefaultChatData> {
|
|
|
319
348
|
}
|
|
320
349
|
}
|
|
321
350
|
|
|
351
|
+
// Attachments bridge — gdy chat woła `.attachFiles({File})`, dodajemy
|
|
352
|
+
// File aggregate do query/mutation deps i budujemy callbacks używane
|
|
353
|
+
// przez listener do: (a) load ArcFile records dla user attachmentów,
|
|
354
|
+
// (b) cache providerFileId po lazy upload.
|
|
355
|
+
let attachmentsBridge: any;
|
|
356
|
+
if (attachFiles) {
|
|
357
|
+
const FileAgg = attachFiles.File;
|
|
358
|
+
if (!allQueryElements.includes(FileAgg)) allQueryElements.push(FileAgg);
|
|
359
|
+
if (!allMutationElements.includes(FileAgg))
|
|
360
|
+
allMutationElements.push(FileAgg);
|
|
361
|
+
attachmentsBridge = {
|
|
362
|
+
loadAttachments: async (ctx: any, fileIds: string[]) => {
|
|
363
|
+
const refs: any[] = [];
|
|
364
|
+
for (const fileId of fileIds) {
|
|
365
|
+
const rec = await ctx
|
|
366
|
+
.query(FileAgg)
|
|
367
|
+
.getById({ _id: fileId });
|
|
368
|
+
if (!rec) continue;
|
|
369
|
+
let providerFileIds: Record<string, string> = {};
|
|
370
|
+
if (typeof rec.providerFileIdsJson === "string") {
|
|
371
|
+
try {
|
|
372
|
+
const parsed = JSON.parse(rec.providerFileIdsJson);
|
|
373
|
+
if (parsed && typeof parsed === "object") providerFileIds = parsed;
|
|
374
|
+
} catch {}
|
|
375
|
+
}
|
|
376
|
+
refs.push({
|
|
377
|
+
fileId,
|
|
378
|
+
name: rec.name,
|
|
379
|
+
mime: rec.mime,
|
|
380
|
+
s3Key: rec.s3Key,
|
|
381
|
+
providerFileIds,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
return refs;
|
|
385
|
+
},
|
|
386
|
+
bindProviderFileId: async (
|
|
387
|
+
ctx: any,
|
|
388
|
+
fileId: string,
|
|
389
|
+
provider: string,
|
|
390
|
+
providerFileId: string,
|
|
391
|
+
) => {
|
|
392
|
+
await ctx
|
|
393
|
+
.mutate(FileAgg)
|
|
394
|
+
.bindProviderFileId({ _id: fileId, provider, providerFileId });
|
|
395
|
+
},
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
322
399
|
const listenerConfig = {
|
|
323
400
|
name,
|
|
324
401
|
messageElement: Message,
|
|
@@ -333,6 +410,7 @@ export class ArcChat<const Data extends ArcChatData = DefaultChatData> {
|
|
|
333
410
|
alias: aliasOverride ?? name,
|
|
334
411
|
recordUsage: aiConfig.recordUsage,
|
|
335
412
|
billTo: billTo ?? undefined,
|
|
413
|
+
attachments: attachmentsBridge,
|
|
336
414
|
};
|
|
337
415
|
|
|
338
416
|
const aiListener = createAiGenerationListener(listenerConfig);
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/// <reference path="../arc.d.ts" />
|
|
2
2
|
import { listener, type ArcContextElement, type ArcFunction } from "@arcote.tech/arc";
|
|
3
3
|
import type {
|
|
4
|
+
ArcFileRef,
|
|
4
5
|
ArcToolAny,
|
|
5
6
|
AssistantContentBlock,
|
|
7
|
+
BoundProviderFile,
|
|
6
8
|
Conversation,
|
|
7
9
|
ConversationTurn,
|
|
8
10
|
LLMProvider,
|
|
@@ -59,6 +61,29 @@ export interface AiGenerationListenerConfig {
|
|
|
59
61
|
* listener can treat the pair as always-present in the call site.
|
|
60
62
|
*/
|
|
61
63
|
billTo?: (tokenParams: Record<string, any>) => string;
|
|
64
|
+
/**
|
|
65
|
+
* Wiązanie z fragmentem `arc-files` — wstrzykiwany przez chat-builder gdy
|
|
66
|
+
* consumer woła `.attachFiles({ File })`.
|
|
67
|
+
*
|
|
68
|
+
* - `loadAttachments(fileIds)` — ładuje `ArcFile` records po fileId'ach
|
|
69
|
+
* (dla `request.files`); zwracamy ArcFileRef z aktualnymi
|
|
70
|
+
* `providerFileIds` z DB.
|
|
71
|
+
* - `bindProviderFileId(fileId, provider, providerFileId)` — wywoływane
|
|
72
|
+
* dla każdego `BoundProviderFile` zwróconego przez adapter (cache
|
|
73
|
+
* lazy uploadu — przy następnej generacji pomijamy upload).
|
|
74
|
+
*/
|
|
75
|
+
attachments?: {
|
|
76
|
+
loadAttachments(
|
|
77
|
+
ctx: any,
|
|
78
|
+
fileIds: string[],
|
|
79
|
+
): Promise<ArcFileRef[]>;
|
|
80
|
+
bindProviderFileId(
|
|
81
|
+
ctx: any,
|
|
82
|
+
fileId: string,
|
|
83
|
+
provider: string,
|
|
84
|
+
providerFileId: string,
|
|
85
|
+
): Promise<void>;
|
|
86
|
+
};
|
|
62
87
|
}
|
|
63
88
|
|
|
64
89
|
// ─── History reconstruction ─────────────────────────────────────
|
|
@@ -233,6 +258,11 @@ interface RunLoopConfig {
|
|
|
233
258
|
recordUsage?: AiGenerationListenerConfig["recordUsage"];
|
|
234
259
|
/** Token-params → scopeId mapper from chat-builder `.billTo(...)`. */
|
|
235
260
|
billTo?: AiGenerationListenerConfig["billTo"];
|
|
261
|
+
/** Attachments bridge — see `AiGenerationListenerConfig.attachments`. */
|
|
262
|
+
attachments?: AiGenerationListenerConfig["attachments"];
|
|
263
|
+
/** ArcFile records dla attachmentów ostatniego user msg (resolved przed
|
|
264
|
+
* generation start; reused per loop iteration). */
|
|
265
|
+
initialAttachments?: ArcFileRef[];
|
|
236
266
|
}
|
|
237
267
|
|
|
238
268
|
async function runGenerationLoop(config: RunLoopConfig) {
|
|
@@ -310,6 +340,13 @@ async function runGenerationLoop(config: RunLoopConfig) {
|
|
|
310
340
|
// robi nic.
|
|
311
341
|
startStream(currentTurnId!);
|
|
312
342
|
|
|
343
|
+
// Files lecą TYLKO w pierwszej iteracji. Provider z continuation
|
|
344
|
+
// (OpenAI Responses) trzyma context server-side; provider w full
|
|
345
|
+
// mode (Claude/Gemini) ignoruje files z warningiem — i tak nie
|
|
346
|
+
// ma sensu duplikować attachmentów w każdym tool_result roundzie.
|
|
347
|
+
const filesForRequest =
|
|
348
|
+
executionCount === 0 ? config.initialAttachments : undefined;
|
|
349
|
+
|
|
313
350
|
const result = await provider.streamComplete(
|
|
314
351
|
{
|
|
315
352
|
model,
|
|
@@ -320,6 +357,9 @@ async function runGenerationLoop(config: RunLoopConfig) {
|
|
|
320
357
|
// Skraca time-to-first-token dla gpt-5 / o-series — pomija reasoning
|
|
321
358
|
// step. Adaptery bez wsparcia ignorują.
|
|
322
359
|
reasoningEffort: "minimal",
|
|
360
|
+
...(filesForRequest && filesForRequest.length > 0
|
|
361
|
+
? { files: filesForRequest }
|
|
362
|
+
: {}),
|
|
323
363
|
},
|
|
324
364
|
(chunk) => {
|
|
325
365
|
if (chunk.type === "text_delta" && chunk.textDelta) {
|
|
@@ -356,6 +396,32 @@ async function runGenerationLoop(config: RunLoopConfig) {
|
|
|
356
396
|
},
|
|
357
397
|
);
|
|
358
398
|
|
|
399
|
+
// Cache lazy-uploaded provider file IDs w ArcFile aggregate — żeby
|
|
400
|
+
// kolejne generacje (i odnowiona historia) pomijały re-upload.
|
|
401
|
+
if (
|
|
402
|
+
config.attachments &&
|
|
403
|
+
result.boundProviderFiles &&
|
|
404
|
+
result.boundProviderFiles.length > 0
|
|
405
|
+
) {
|
|
406
|
+
for (const bound of result.boundProviderFiles) {
|
|
407
|
+
try {
|
|
408
|
+
await config.attachments.bindProviderFileId(
|
|
409
|
+
ctx,
|
|
410
|
+
bound.fileId,
|
|
411
|
+
bound.provider,
|
|
412
|
+
bound.providerFileId,
|
|
413
|
+
);
|
|
414
|
+
} catch (err) {
|
|
415
|
+
// Best-effort cache — failure tu nie psuje turn'u, tylko
|
|
416
|
+
// następny request uploadowi plik ponownie.
|
|
417
|
+
console.warn(
|
|
418
|
+
"[arc-chat] attachments.bindProviderFileId failed:",
|
|
419
|
+
err,
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
359
425
|
// Append to local history so the next iteration sees this turn.
|
|
360
426
|
const assistantTurn: ConversationTurn = {
|
|
361
427
|
role: "assistant",
|
|
@@ -570,6 +636,14 @@ export function createAiGenerationListener(config: AiGenerationListenerConfig) {
|
|
|
570
636
|
const newTurnsStartIdx = history.length;
|
|
571
637
|
history.push({ role: "user", content: userContent });
|
|
572
638
|
|
|
639
|
+
// Załaduj ArcFile records dla attachmentów ostatniego user msg
|
|
640
|
+
// (jeśli consumer wired `attachments` bridge przez `.attachFiles({File})`).
|
|
641
|
+
const initialAttachments = await resolveAttachments(
|
|
642
|
+
ctx,
|
|
643
|
+
config.attachments,
|
|
644
|
+
(event.payload as { attachmentsJson?: string }).attachmentsJson,
|
|
645
|
+
);
|
|
646
|
+
|
|
573
647
|
await runGenerationLoop({
|
|
574
648
|
ctx,
|
|
575
649
|
messageElement,
|
|
@@ -588,6 +662,8 @@ export function createAiGenerationListener(config: AiGenerationListenerConfig) {
|
|
|
588
662
|
alias: config.alias,
|
|
589
663
|
recordUsage: config.recordUsage,
|
|
590
664
|
billTo: config.billTo,
|
|
665
|
+
attachments: config.attachments,
|
|
666
|
+
initialAttachments,
|
|
591
667
|
preCreatedAssistantMessageId: (
|
|
592
668
|
event.payload as { assistantMessageId?: string }
|
|
593
669
|
).assistantMessageId,
|
|
@@ -595,6 +671,36 @@ export function createAiGenerationListener(config: AiGenerationListenerConfig) {
|
|
|
595
671
|
});
|
|
596
672
|
}
|
|
597
673
|
|
|
674
|
+
/**
|
|
675
|
+
* Parsuje `attachmentsJson` (string[] fileIds) i deleguje do
|
|
676
|
+
* `attachments.loadAttachments(...)`. Brak attachments bridge ALBO brak
|
|
677
|
+
* JSON ALBO pusta lista → zwraca undefined (request bez `files`).
|
|
678
|
+
*/
|
|
679
|
+
async function resolveAttachments(
|
|
680
|
+
ctx: any,
|
|
681
|
+
attachments: AiGenerationListenerConfig["attachments"],
|
|
682
|
+
attachmentsJson: string | undefined,
|
|
683
|
+
): Promise<ArcFileRef[] | undefined> {
|
|
684
|
+
if (!attachments || !attachmentsJson) return undefined;
|
|
685
|
+
let fileIds: string[] = [];
|
|
686
|
+
try {
|
|
687
|
+
const parsed = JSON.parse(attachmentsJson);
|
|
688
|
+
if (Array.isArray(parsed)) {
|
|
689
|
+
fileIds = parsed.filter((x): x is string => typeof x === "string");
|
|
690
|
+
}
|
|
691
|
+
} catch {
|
|
692
|
+
return undefined;
|
|
693
|
+
}
|
|
694
|
+
if (fileIds.length === 0) return undefined;
|
|
695
|
+
try {
|
|
696
|
+
const refs = await attachments.loadAttachments(ctx, fileIds);
|
|
697
|
+
return refs.length > 0 ? refs : undefined;
|
|
698
|
+
} catch (err) {
|
|
699
|
+
console.warn("[arc-chat] attachments.loadAttachments failed:", err);
|
|
700
|
+
return undefined;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
598
704
|
// ─── Listener B: userResponded → AI resume ──────────────────────
|
|
599
705
|
|
|
600
706
|
export function createAiResumeListener(config: AiGenerationListenerConfig) {
|
|
@@ -671,6 +777,7 @@ export function createAiResumeListener(config: AiGenerationListenerConfig) {
|
|
|
671
777
|
alias: config.alias,
|
|
672
778
|
recordUsage: config.recordUsage,
|
|
673
779
|
billTo: config.billTo,
|
|
780
|
+
attachments: config.attachments,
|
|
674
781
|
preCreatedAssistantMessageId: (
|
|
675
782
|
event.payload as { assistantMessageId?: string }
|
|
676
783
|
).assistantMessageId,
|
|
@@ -622,6 +622,9 @@ export function createChatComponent(
|
|
|
622
622
|
scopeId,
|
|
623
623
|
content,
|
|
624
624
|
model: options.model,
|
|
625
|
+
...(options.attachments && options.attachments.length > 0
|
|
626
|
+
? { attachmentsJson: JSON.stringify(options.attachments) }
|
|
627
|
+
: {}),
|
|
625
628
|
});
|
|
626
629
|
// Reszta dzieje się przez auto-subscribe effect powyżej: mutacja
|
|
627
630
|
// emit'uje `assistantTurnStarted` → DB query pushuje fresh assistant
|