@pikku/assistant-ui 0.12.4 → 0.12.6
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 +12 -1
- package/dist/cjs/index.d.ts +3 -0
- package/dist/cjs/index.js +6 -1
- package/dist/cjs/model-capabilities.d.ts +1 -0
- package/dist/cjs/model-capabilities.js +16 -0
- package/dist/cjs/pikku-agent-chat.d.ts +4 -0
- package/dist/cjs/pikku-agent-chat.js +91 -63
- package/dist/cjs/use-file-attachment.d.ts +26 -0
- package/dist/cjs/use-file-attachment.js +99 -0
- package/dist/cjs/use-pikku-agent-runtime.d.ts +4 -0
- package/dist/cjs/use-pikku-agent-runtime.js +72 -35
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/model-capabilities.d.ts +1 -0
- package/dist/esm/model-capabilities.js +13 -0
- package/dist/esm/pikku-agent-chat.d.ts +4 -0
- package/dist/esm/pikku-agent-chat.js +102 -67
- package/dist/esm/use-file-attachment.d.ts +26 -0
- package/dist/esm/use-file-attachment.js +85 -0
- package/dist/esm/use-pikku-agent-runtime.d.ts +4 -0
- package/dist/esm/use-pikku-agent-runtime.js +72 -19
- package/dist/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/index.ts +3 -0
- package/src/model-capabilities.ts +14 -0
- package/src/pikku-agent-chat.tsx +362 -156
- package/src/use-file-attachment.ts +110 -0
- package/src/use-pikku-agent-runtime.ts +109 -15
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { useState, useCallback, useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
export type PendingFile = {
|
|
4
|
+
id: string
|
|
5
|
+
name: string
|
|
6
|
+
mimeType: string
|
|
7
|
+
previewUrl: string
|
|
8
|
+
contentUrl: string
|
|
9
|
+
isImage: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type UploadAttachmentFn = (args: {
|
|
13
|
+
contentType: string
|
|
14
|
+
sizeBytes: number
|
|
15
|
+
}) => Promise<{
|
|
16
|
+
uploadUrl: string
|
|
17
|
+
signedReadUrl: string
|
|
18
|
+
uploadMethod?: string
|
|
19
|
+
}>
|
|
20
|
+
|
|
21
|
+
export const INLINE_SIZE_LIMIT = 1 * 1024 * 1024
|
|
22
|
+
|
|
23
|
+
export function useFileAttachment(upload: UploadAttachmentFn) {
|
|
24
|
+
const [pendingFiles, setPendingFiles] = useState<PendingFile[]>([])
|
|
25
|
+
const [uploading, setUploading] = useState(false)
|
|
26
|
+
const [uploadError, setUploadError] = useState<string | null>(null)
|
|
27
|
+
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
28
|
+
|
|
29
|
+
const handleFileChange = useCallback(
|
|
30
|
+
async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
31
|
+
const files = Array.from(e.target.files ?? [])
|
|
32
|
+
e.target.value = ''
|
|
33
|
+
if (!files.length) return
|
|
34
|
+
setUploading(true)
|
|
35
|
+
setUploadError(null)
|
|
36
|
+
try {
|
|
37
|
+
for (const file of files) {
|
|
38
|
+
const previewUrl = URL.createObjectURL(file)
|
|
39
|
+
let contentUrl: string
|
|
40
|
+
|
|
41
|
+
if (
|
|
42
|
+
file.type.startsWith('image/') &&
|
|
43
|
+
file.size <= INLINE_SIZE_LIMIT
|
|
44
|
+
) {
|
|
45
|
+
contentUrl = await new Promise<string>((resolve, reject) => {
|
|
46
|
+
const reader = new FileReader()
|
|
47
|
+
reader.onload = (ev) => resolve(ev.target!.result as string)
|
|
48
|
+
reader.onerror = reject
|
|
49
|
+
reader.readAsDataURL(file)
|
|
50
|
+
})
|
|
51
|
+
} else {
|
|
52
|
+
const { uploadUrl, signedReadUrl, uploadMethod } = await upload({
|
|
53
|
+
contentType: file.type,
|
|
54
|
+
sizeBytes: file.size,
|
|
55
|
+
})
|
|
56
|
+
const resp = await fetch(uploadUrl, {
|
|
57
|
+
method: uploadMethod ?? 'PUT',
|
|
58
|
+
body: file,
|
|
59
|
+
headers: { 'Content-Type': file.type },
|
|
60
|
+
})
|
|
61
|
+
if (!resp.ok) throw new Error(`Upload failed (${resp.status})`)
|
|
62
|
+
contentUrl = signedReadUrl
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
setPendingFiles((prev) => [
|
|
66
|
+
...prev,
|
|
67
|
+
{
|
|
68
|
+
id: `file_${Date.now().toString(36)}`,
|
|
69
|
+
name: file.name,
|
|
70
|
+
mimeType: file.type,
|
|
71
|
+
previewUrl,
|
|
72
|
+
contentUrl,
|
|
73
|
+
isImage: file.type.startsWith('image/'),
|
|
74
|
+
},
|
|
75
|
+
])
|
|
76
|
+
}
|
|
77
|
+
} catch (err) {
|
|
78
|
+
setUploadError(err instanceof Error ? err.message : 'Upload failed')
|
|
79
|
+
} finally {
|
|
80
|
+
setUploading(false)
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
[upload]
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
const removeFile = useCallback((id: string) => {
|
|
87
|
+
setPendingFiles((prev) => {
|
|
88
|
+
const f = prev.find((x) => x.id === id)
|
|
89
|
+
if (f) URL.revokeObjectURL(f.previewUrl)
|
|
90
|
+
return prev.filter((x) => x.id !== id)
|
|
91
|
+
})
|
|
92
|
+
}, [])
|
|
93
|
+
|
|
94
|
+
const clearFiles = useCallback(() => {
|
|
95
|
+
setPendingFiles((prev) => {
|
|
96
|
+
prev.forEach((f) => URL.revokeObjectURL(f.previewUrl))
|
|
97
|
+
return []
|
|
98
|
+
})
|
|
99
|
+
}, [])
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
pendingFiles,
|
|
103
|
+
uploading,
|
|
104
|
+
uploadError,
|
|
105
|
+
fileInputRef,
|
|
106
|
+
handleFileChange,
|
|
107
|
+
removeFile,
|
|
108
|
+
clearFiles,
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -23,6 +23,10 @@ export interface PikkuAgentRuntimeOptions {
|
|
|
23
23
|
headers?: Record<string, string>
|
|
24
24
|
model?: string
|
|
25
25
|
temperature?: number
|
|
26
|
+
/** Structured context injected into the agent's system instructions.
|
|
27
|
+
* Provide upfront state (e.g. current org/project/branch/deployment IDs)
|
|
28
|
+
* so the agent can call tools without asking the user. */
|
|
29
|
+
context?: string
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
export interface PendingApproval {
|
|
@@ -87,6 +91,17 @@ type ToolCall = {
|
|
|
87
91
|
isError?: boolean
|
|
88
92
|
}
|
|
89
93
|
|
|
94
|
+
type StructuredPart =
|
|
95
|
+
| {
|
|
96
|
+
type: 'generative-ui'
|
|
97
|
+
spec: unknown
|
|
98
|
+
}
|
|
99
|
+
| {
|
|
100
|
+
type: 'data'
|
|
101
|
+
name: string
|
|
102
|
+
data: unknown
|
|
103
|
+
}
|
|
104
|
+
|
|
90
105
|
/**
|
|
91
106
|
* Shared helper: consume an SSE stream and populate text/toolCalls.
|
|
92
107
|
* Returns an array of PendingApprovals when the stream requests them, or empty when done.
|
|
@@ -95,6 +110,7 @@ async function processStream(
|
|
|
95
110
|
reader: ReadableStreamDefaultReader<Uint8Array>,
|
|
96
111
|
text: { value: string },
|
|
97
112
|
toolCalls: ToolCall[],
|
|
113
|
+
structuredParts: StructuredPart[],
|
|
98
114
|
yieldContent: () => void,
|
|
99
115
|
onFinish?: () => void
|
|
100
116
|
): Promise<PendingApproval[]> {
|
|
@@ -140,6 +156,31 @@ async function processStream(
|
|
|
140
156
|
}
|
|
141
157
|
break
|
|
142
158
|
}
|
|
159
|
+
case 'generative-ui': {
|
|
160
|
+
const nextPart = {
|
|
161
|
+
type: 'generative-ui' as const,
|
|
162
|
+
spec: event.spec,
|
|
163
|
+
}
|
|
164
|
+
const existingIndex = structuredParts.findIndex(
|
|
165
|
+
(part) => part.type === 'generative-ui'
|
|
166
|
+
)
|
|
167
|
+
if (existingIndex === -1) structuredParts.push(nextPart)
|
|
168
|
+
else structuredParts[existingIndex] = nextPart
|
|
169
|
+
break
|
|
170
|
+
}
|
|
171
|
+
case 'data': {
|
|
172
|
+
const nextPart = {
|
|
173
|
+
type: 'data' as const,
|
|
174
|
+
name: event.name,
|
|
175
|
+
data: event.data,
|
|
176
|
+
}
|
|
177
|
+
const existingIndex = structuredParts.findIndex(
|
|
178
|
+
(part) => part.type === 'data' && part.name === event.name
|
|
179
|
+
)
|
|
180
|
+
if (existingIndex === -1) structuredParts.push(nextPart)
|
|
181
|
+
else structuredParts[existingIndex] = nextPart
|
|
182
|
+
break
|
|
183
|
+
}
|
|
143
184
|
case 'approval-request':
|
|
144
185
|
pendingApprovals.push({
|
|
145
186
|
toolCallId: event.toolCallId,
|
|
@@ -237,13 +278,39 @@ export function resolvePikkuToolStatus(
|
|
|
237
278
|
return { type: 'completed' }
|
|
238
279
|
}
|
|
239
280
|
|
|
240
|
-
function
|
|
281
|
+
function buildRichContent(
|
|
282
|
+
text: { value: string },
|
|
283
|
+
structuredParts: StructuredPart[],
|
|
284
|
+
toolCalls: ToolCall[]
|
|
285
|
+
): any[] {
|
|
241
286
|
const content: any[] = []
|
|
242
287
|
if (text.value) content.push({ type: 'text' as const, text: text.value })
|
|
288
|
+
content.push(...structuredParts)
|
|
243
289
|
content.push(...toolCalls)
|
|
244
290
|
return content
|
|
245
291
|
}
|
|
246
292
|
|
|
293
|
+
function buildContentFromAgentResult(result: unknown): any[] {
|
|
294
|
+
const content: any[] = []
|
|
295
|
+
|
|
296
|
+
if (typeof result === 'string') {
|
|
297
|
+
if (result) content.push({ type: 'text' as const, text: result })
|
|
298
|
+
return content
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (!result || typeof result !== 'object') return content
|
|
302
|
+
|
|
303
|
+
const record = result as Record<string, unknown>
|
|
304
|
+
if (typeof record.text === 'string' && record.text) {
|
|
305
|
+
content.push({ type: 'text' as const, text: record.text })
|
|
306
|
+
}
|
|
307
|
+
if (record.ui != null) {
|
|
308
|
+
content.push({ type: 'generative-ui' as const, spec: record.ui })
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return content
|
|
312
|
+
}
|
|
313
|
+
|
|
247
314
|
function createPikkuStreamingAdapter(
|
|
248
315
|
optionsRef: React.RefObject<PikkuAgentRuntimeOptions>,
|
|
249
316
|
pendingApprovalsRef: React.RefObject<PendingApproval[]>,
|
|
@@ -281,6 +348,7 @@ function createPikkuStreamingAdapter(
|
|
|
281
348
|
// The last resume triggers continuation (next LLM step).
|
|
282
349
|
let lastText = { value: '' }
|
|
283
350
|
let lastToolCalls: ToolCall[] = []
|
|
351
|
+
let lastStructuredParts: StructuredPart[] = []
|
|
284
352
|
let nextApprovals: PendingApproval[] = []
|
|
285
353
|
|
|
286
354
|
for (let i = 0; i < decisions.length; i++) {
|
|
@@ -320,11 +388,13 @@ function createPikkuStreamingAdapter(
|
|
|
320
388
|
|
|
321
389
|
const text = { value: '' }
|
|
322
390
|
const toolCalls: ToolCall[] = []
|
|
391
|
+
const structuredParts: StructuredPart[] = []
|
|
323
392
|
const reader = resumeResponse.body.getReader()
|
|
324
393
|
const streamApprovals = await processStream(
|
|
325
394
|
reader,
|
|
326
395
|
text,
|
|
327
396
|
toolCalls,
|
|
397
|
+
structuredParts,
|
|
328
398
|
() => {},
|
|
329
399
|
i === decisions.length - 1
|
|
330
400
|
? (onFinishRef.current ?? undefined)
|
|
@@ -334,13 +404,18 @@ function createPikkuStreamingAdapter(
|
|
|
334
404
|
// Keep the last resume's output (it has continuation content)
|
|
335
405
|
lastText = text
|
|
336
406
|
lastToolCalls = toolCalls
|
|
407
|
+
lastStructuredParts = structuredParts
|
|
337
408
|
if (streamApprovals.length > 0) {
|
|
338
409
|
nextApprovals = streamApprovals
|
|
339
410
|
}
|
|
340
411
|
}
|
|
341
412
|
|
|
342
413
|
// Build content from the last resume's output
|
|
343
|
-
const content =
|
|
414
|
+
const content = buildRichContent(
|
|
415
|
+
lastText,
|
|
416
|
+
lastStructuredParts,
|
|
417
|
+
lastToolCalls
|
|
418
|
+
)
|
|
344
419
|
|
|
345
420
|
if (nextApprovals.length > 0) {
|
|
346
421
|
// More approvals from continuation — show them
|
|
@@ -372,7 +447,11 @@ function createPikkuStreamingAdapter(
|
|
|
372
447
|
}
|
|
373
448
|
}
|
|
374
449
|
|
|
375
|
-
const updatedContent =
|
|
450
|
+
const updatedContent = buildRichContent(
|
|
451
|
+
lastText,
|
|
452
|
+
lastStructuredParts,
|
|
453
|
+
lastToolCalls
|
|
454
|
+
)
|
|
376
455
|
yield {
|
|
377
456
|
content: updatedContent,
|
|
378
457
|
status: {
|
|
@@ -411,6 +490,7 @@ function createPikkuStreamingAdapter(
|
|
|
411
490
|
resourceId: opts.resourceId,
|
|
412
491
|
model: opts.model,
|
|
413
492
|
temperature: opts.temperature,
|
|
493
|
+
...(opts.context ? { context: opts.context } : {}),
|
|
414
494
|
}),
|
|
415
495
|
signal: abortSignal,
|
|
416
496
|
credentials: opts.credentials,
|
|
@@ -451,9 +531,10 @@ function createPikkuStreamingAdapter(
|
|
|
451
531
|
|
|
452
532
|
const text = { value: '' }
|
|
453
533
|
const toolCalls: ToolCall[] = []
|
|
534
|
+
const structuredParts: StructuredPart[] = []
|
|
454
535
|
let pendingContent: any[] | null = null
|
|
455
536
|
const yieldContent = () => {
|
|
456
|
-
const content =
|
|
537
|
+
const content = buildRichContent(text, structuredParts, toolCalls)
|
|
457
538
|
if (content.length > 0) pendingContent = content
|
|
458
539
|
}
|
|
459
540
|
|
|
@@ -462,6 +543,7 @@ function createPikkuStreamingAdapter(
|
|
|
462
543
|
reader,
|
|
463
544
|
text,
|
|
464
545
|
toolCalls,
|
|
546
|
+
structuredParts,
|
|
465
547
|
yieldContent,
|
|
466
548
|
onFinishRef.current ?? undefined
|
|
467
549
|
)
|
|
@@ -516,7 +598,7 @@ function createPikkuStreamingAdapter(
|
|
|
516
598
|
}
|
|
517
599
|
}
|
|
518
600
|
|
|
519
|
-
const content =
|
|
601
|
+
const content = buildRichContent(text, structuredParts, toolCalls)
|
|
520
602
|
yield {
|
|
521
603
|
content,
|
|
522
604
|
status: {
|
|
@@ -596,7 +678,11 @@ export const convertDbMessages = (dbMessages: any[]): ThreadMessageLike[] => {
|
|
|
596
678
|
const parts: any[] = []
|
|
597
679
|
|
|
598
680
|
if (msg.content) {
|
|
599
|
-
|
|
681
|
+
if (Array.isArray(msg.content)) {
|
|
682
|
+
parts.push(...msg.content)
|
|
683
|
+
} else {
|
|
684
|
+
parts.push({ type: 'text' as const, text: msg.content })
|
|
685
|
+
}
|
|
600
686
|
}
|
|
601
687
|
|
|
602
688
|
if (Array.isArray(msg.toolCalls)) {
|
|
@@ -732,6 +818,7 @@ function createPikkuNonStreamingAdapter(
|
|
|
732
818
|
// Resume uses SSE (same as streaming mode)
|
|
733
819
|
let lastText = { value: '' }
|
|
734
820
|
let lastToolCalls: ToolCall[] = []
|
|
821
|
+
let lastStructuredParts: StructuredPart[] = []
|
|
735
822
|
let nextApprovals: PendingApproval[] = []
|
|
736
823
|
|
|
737
824
|
for (let i = 0; i < decisions.length; i++) {
|
|
@@ -770,11 +857,13 @@ function createPikkuNonStreamingAdapter(
|
|
|
770
857
|
|
|
771
858
|
const text = { value: '' }
|
|
772
859
|
const toolCalls: ToolCall[] = []
|
|
860
|
+
const structuredParts: StructuredPart[] = []
|
|
773
861
|
const reader = resumeResponse.body.getReader()
|
|
774
862
|
const streamApprovals = await processStream(
|
|
775
863
|
reader,
|
|
776
864
|
text,
|
|
777
865
|
toolCalls,
|
|
866
|
+
structuredParts,
|
|
778
867
|
() => {},
|
|
779
868
|
i === decisions.length - 1
|
|
780
869
|
? (onFinishRef.current ?? undefined)
|
|
@@ -783,12 +872,17 @@ function createPikkuNonStreamingAdapter(
|
|
|
783
872
|
|
|
784
873
|
lastText = text
|
|
785
874
|
lastToolCalls = toolCalls
|
|
875
|
+
lastStructuredParts = structuredParts
|
|
786
876
|
if (streamApprovals.length > 0) {
|
|
787
877
|
nextApprovals = streamApprovals
|
|
788
878
|
}
|
|
789
879
|
}
|
|
790
880
|
|
|
791
|
-
const content =
|
|
881
|
+
const content = buildRichContent(
|
|
882
|
+
lastText,
|
|
883
|
+
lastStructuredParts,
|
|
884
|
+
lastToolCalls
|
|
885
|
+
)
|
|
792
886
|
|
|
793
887
|
if (nextApprovals.length > 0) {
|
|
794
888
|
pendingApprovalsRef.current = nextApprovals
|
|
@@ -818,7 +912,11 @@ function createPikkuNonStreamingAdapter(
|
|
|
818
912
|
}
|
|
819
913
|
}
|
|
820
914
|
|
|
821
|
-
const updatedContent =
|
|
915
|
+
const updatedContent = buildRichContent(
|
|
916
|
+
lastText,
|
|
917
|
+
lastStructuredParts,
|
|
918
|
+
lastToolCalls
|
|
919
|
+
)
|
|
822
920
|
yield {
|
|
823
921
|
content: updatedContent,
|
|
824
922
|
status: {
|
|
@@ -855,6 +953,7 @@ function createPikkuNonStreamingAdapter(
|
|
|
855
953
|
resourceId: opts.resourceId,
|
|
856
954
|
model: opts.model,
|
|
857
955
|
temperature: opts.temperature,
|
|
956
|
+
...(opts.context ? { context: opts.context } : {}),
|
|
858
957
|
}),
|
|
859
958
|
signal: abortSignal,
|
|
860
959
|
credentials: opts.credentials,
|
|
@@ -884,9 +983,7 @@ function createPikkuNonStreamingAdapter(
|
|
|
884
983
|
}))
|
|
885
984
|
|
|
886
985
|
const content: any[] = []
|
|
887
|
-
|
|
888
|
-
content.push({ type: 'text' as const, text: json.result })
|
|
889
|
-
}
|
|
986
|
+
content.push(...buildContentFromAgentResult(json.result))
|
|
890
987
|
content.push(...toolCalls)
|
|
891
988
|
|
|
892
989
|
yield {
|
|
@@ -901,10 +998,7 @@ function createPikkuNonStreamingAdapter(
|
|
|
901
998
|
|
|
902
999
|
// No approvals — yield complete content
|
|
903
1000
|
onFinishRef.current?.()
|
|
904
|
-
const content
|
|
905
|
-
if (json.result) {
|
|
906
|
-
content.push({ type: 'text' as const, text: String(json.result) })
|
|
907
|
-
}
|
|
1001
|
+
const content = buildContentFromAgentResult(json.result)
|
|
908
1002
|
if (content.length > 0) {
|
|
909
1003
|
yield { content }
|
|
910
1004
|
}
|