bleam 0.0.11 → 0.0.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/dist/ai.cjs +2315 -2173
- package/dist/ai.d.cts +227 -322
- package/dist/ai.d.ts +227 -322
- package/dist/ai.js +2310 -2159
- package/dist/app-storage-D8W4n8ey.cjs +39 -0
- package/dist/app-storage-Isi5Bo0R.js +34 -0
- package/dist/cli.cjs +148 -22
- package/dist/cli.d.cts +14 -1
- package/dist/cli.d.ts +13 -0
- package/dist/cli.js +148 -22
- package/dist/config.d.cts +1 -1
- package/dist/elements-CFk0QHw0.d.cts +127 -0
- package/dist/{ui-CHc4xEs_.d.ts → elements-ClGQ41Sc.d.ts} +84 -49
- package/dist/{ui.cjs → elements.cjs} +307 -168
- package/dist/elements.d.cts +2 -0
- package/dist/elements.d.ts +2 -0
- package/dist/{ui.js → elements.js} +306 -169
- package/dist/{files-Ds1wT8C2.js → files-DErLhzCB.js} +11 -12
- package/dist/{files-Bo7h9fik.cjs → files-lMk-CpL_.cjs} +16 -11
- package/dist/files.cjs +1 -1
- package/dist/files.d.cts +1 -1
- package/dist/files.js +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/schema-B7ELMpuI.js +226 -0
- package/dist/schema-B7SLUBLN.cjs +286 -0
- package/dist/schema-BWsDPc6c.d.cts +125 -0
- package/dist/schema-LxnzAfgw.d.ts +125 -0
- package/dist/schema.cjs +10 -2
- package/dist/schema.d.cts +2 -2
- package/dist/schema.d.ts +2 -2
- package/dist/schema.js +2 -2
- package/dist/secrets.cjs +146 -0
- package/dist/secrets.d.cts +14 -0
- package/dist/secrets.d.ts +14 -0
- package/dist/secrets.js +142 -0
- package/dist/state-BZYyrE2-.cjs +936 -0
- package/dist/state-DkaRFkZJ.js +843 -0
- package/dist/state.cjs +17 -12
- package/dist/state.d.cts +144 -140
- package/dist/state.d.ts +145 -140
- package/dist/state.js +4 -3
- package/dist/window.d.cts +1 -1
- package/dist/window.d.ts +3 -3
- package/package.json +13 -6
- package/templates/basic/app/index.tsx +2 -2
- package/templates/foundation-models/app/index.tsx +78 -16
- package/templates/image-generation/app/index.tsx +4 -4
- package/templates/native/ios/Bleam.xcodeproj/project.pbxproj +46 -46
- package/templates/native/ios/Podfile.lock +173 -173
- package/templates/native/modules/bleam-runtime/ios/AIModule.swift +42 -365
- package/templates/state/app/index.tsx +2 -2
- package/templates/text-generation/app/index.tsx +83 -52
- package/templates/updates/README.md +1 -1
- package/dist/schema-Bo5Jvqus.js +0 -90
- package/dist/schema-CYh6n8GS.d.ts +0 -58
- package/dist/schema-oeOrd3i1.d.cts +0 -58
- package/dist/schema-rQ13mrpD.cjs +0 -102
- package/dist/state-Bx0VlTlO.cjs +0 -852
- package/dist/state-CAwe-Vw1.js +0 -767
- package/dist/ui-1WepaMS4.d.cts +0 -92
- package/dist/ui.d.cts +0 -2
- package/dist/ui.d.ts +0 -2
- /package/dist/{config-CufOVJV3.d.cts → config-COcRnn5a.d.cts} +0 -0
- /package/dist/{files-Dt5mbzLq.d.cts → files-DwA7pzr3.d.cts} +0 -0
- /package/dist/{native-sqlite-yQLD5s9i.cjs → native-sqlite-Dw--FI9a.cjs} +0 -0
- /package/dist/{native-sqlite-xcGdamRD.js → native-sqlite-WzRNzCSh.js} +0 -0
|
@@ -9,96 +9,10 @@ import FoundationModels
|
|
|
9
9
|
|
|
10
10
|
public final class AIModule: Module {
|
|
11
11
|
@MainActor private let textGeneration = TextGenerationCoordinator()
|
|
12
|
-
private static var appGroupIdentifier: String {
|
|
13
|
-
Bundle.main.object(forInfoDictionaryKey: "AppGroupIdentifier") as? String ?? "group.dev.bleam.app"
|
|
14
|
-
}
|
|
15
12
|
|
|
16
13
|
public func definition() -> ModuleDefinition {
|
|
17
14
|
Name("AI")
|
|
18
|
-
Events("imagegenProgress", "textGenerationUpdate", "localTextGenerationUpdate"
|
|
19
|
-
|
|
20
|
-
Function("isAvailable") { () -> Bool in
|
|
21
|
-
#if canImport(FoundationModels)
|
|
22
|
-
if #available(iOS 26, *) {
|
|
23
|
-
return SystemLanguageModel.default.availability == .available
|
|
24
|
-
}
|
|
25
|
-
#endif
|
|
26
|
-
return false
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
Function("availability") { () -> [String: Any] in
|
|
30
|
-
#if canImport(FoundationModels)
|
|
31
|
-
if #available(iOS 26, *) {
|
|
32
|
-
switch SystemLanguageModel.default.availability {
|
|
33
|
-
case .available:
|
|
34
|
-
return ["available": true]
|
|
35
|
-
case .unavailable(.deviceNotEligible):
|
|
36
|
-
return ["available": false, "reason": "device_not_eligible"]
|
|
37
|
-
case .unavailable(.appleIntelligenceNotEnabled):
|
|
38
|
-
return ["available": false, "reason": "apple_intelligence_not_enabled"]
|
|
39
|
-
case .unavailable(.modelNotReady):
|
|
40
|
-
return ["available": false, "reason": "model_not_ready"]
|
|
41
|
-
case .unavailable:
|
|
42
|
-
return ["available": false, "reason": "unknown"]
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
#endif
|
|
46
|
-
return ["available": false, "reason": "unknown"]
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
AsyncFunction("generate") { [weak self] (request: TextGenerationRequest, promise: Promise) in
|
|
50
|
-
#if canImport(FoundationModels)
|
|
51
|
-
if #available(iOS 26, *) {
|
|
52
|
-
guard SystemLanguageModel.default.availability == .available else {
|
|
53
|
-
promise.reject("MODEL_UNAVAILABLE", "Apple Intelligence model is not available")
|
|
54
|
-
return
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
guard !request.prompt.isEmpty else {
|
|
58
|
-
promise.reject("INVALID_MESSAGE", "Expected prompt")
|
|
59
|
-
return
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
let generationID = request.id ?? UUID().uuidString
|
|
63
|
-
let task = Task { @MainActor [weak self] in
|
|
64
|
-
guard let self else { return }
|
|
65
|
-
do {
|
|
66
|
-
let tools = try Self.tools(
|
|
67
|
-
from: request.tools,
|
|
68
|
-
generationID: generationID,
|
|
69
|
-
coordinator: self.textGeneration,
|
|
70
|
-
emit: { [weak self] event in self?.sendEvent("toolCall", event) }
|
|
71
|
-
)
|
|
72
|
-
let session = Self.session(system: request.system, tools: tools)
|
|
73
|
-
let options = try Self.generationOptions(from: request)
|
|
74
|
-
|
|
75
|
-
if let schema = request.outputSchema {
|
|
76
|
-
let generationSchema = try Self.generationSchema(from: schema)
|
|
77
|
-
let response = try await session.respond(
|
|
78
|
-
to: request.prompt,
|
|
79
|
-
schema: generationSchema,
|
|
80
|
-
includeSchemaInPrompt: true,
|
|
81
|
-
options: options
|
|
82
|
-
)
|
|
83
|
-
promise.resolve(try Self.value(from: response.content, schema: schema))
|
|
84
|
-
return
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
let response = try await session.respond(to: request.prompt, options: options)
|
|
88
|
-
promise.resolve(response.content)
|
|
89
|
-
} catch {
|
|
90
|
-
Self.reject(error, promise: promise)
|
|
91
|
-
}
|
|
92
|
-
self.textGeneration.finish(generationID)
|
|
93
|
-
}
|
|
94
|
-
Task { @MainActor [weak self] in self?.textGeneration.start(generationID, task: task) }
|
|
95
|
-
} else {
|
|
96
|
-
promise.reject("UNSUPPORTED_OS", "Apple Foundation Models require iOS 26 or newer")
|
|
97
|
-
}
|
|
98
|
-
#else
|
|
99
|
-
promise.reject("UNSUPPORTED_OS", "Apple Foundation Models are unavailable in this build")
|
|
100
|
-
#endif
|
|
101
|
-
}
|
|
15
|
+
Events("imagegenProgress", "textGenerationUpdate", "localTextGenerationUpdate")
|
|
102
16
|
|
|
103
17
|
AsyncFunction("streamGenerate") { [weak self] (request: TextGenerationRequest, promise: Promise) in
|
|
104
18
|
#if canImport(FoundationModels)
|
|
@@ -116,13 +30,7 @@ public final class AIModule: Module {
|
|
|
116
30
|
let task = Task { @MainActor [weak self] in
|
|
117
31
|
guard let self else { return }
|
|
118
32
|
do {
|
|
119
|
-
let
|
|
120
|
-
from: request.tools,
|
|
121
|
-
generationID: generationID,
|
|
122
|
-
coordinator: self.textGeneration,
|
|
123
|
-
emit: { [weak self] event in self?.sendEvent("toolCall", event) }
|
|
124
|
-
)
|
|
125
|
-
let session = Self.session(system: request.system, tools: tools)
|
|
33
|
+
let session = try Self.session(system: request.system, messages: request.messages)
|
|
126
34
|
let options = try Self.generationOptions(from: request)
|
|
127
35
|
let stream = session.streamResponse(to: request.prompt, options: options)
|
|
128
36
|
var content = ""
|
|
@@ -152,23 +60,6 @@ public final class AIModule: Module {
|
|
|
152
60
|
return foundationCanceled || localCanceled
|
|
153
61
|
}
|
|
154
62
|
|
|
155
|
-
AsyncFunction("resolveToolCall") { [weak self] (request: ToolResultRequest) -> Bool in
|
|
156
|
-
await self?.textGeneration.resolve(request) ?? false
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
AsyncFunction("generateLocalText") { (request: LocalTextGenerationRequest, promise: Promise) in
|
|
160
|
-
Task { @MainActor in
|
|
161
|
-
do {
|
|
162
|
-
let event = try await ImageGenerationServiceClient.shared.runText(
|
|
163
|
-
request: Self.bonsaiTextRequest(from: request)
|
|
164
|
-
)
|
|
165
|
-
promise.resolve(event.tokenIDs)
|
|
166
|
-
} catch {
|
|
167
|
-
Self.rejectLocalTextGeneration(error, promise: promise)
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
63
|
AsyncFunction("streamLocalText") { [weak self] (request: LocalTextGenerationRequest, promise: Promise) in
|
|
173
64
|
Task { @MainActor in
|
|
174
65
|
do {
|
|
@@ -488,13 +379,15 @@ public final class AIModule: Module {
|
|
|
488
379
|
|
|
489
380
|
private static func workerRequest(from request: FluxImageRequest) throws -> GenerationWorkerRequest {
|
|
490
381
|
let jobID = UUID()
|
|
491
|
-
guard let
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
382
|
+
guard let cacheDirectory = FileManager.default.urls(
|
|
383
|
+
for: .cachesDirectory,
|
|
384
|
+
in: .userDomainMask
|
|
385
|
+
).first else {
|
|
386
|
+
throw ModuleError.imageGeneration("Generation cache is unavailable")
|
|
495
387
|
}
|
|
496
388
|
|
|
497
|
-
let stagingDirectory =
|
|
389
|
+
let stagingDirectory = cacheDirectory
|
|
390
|
+
.appendingPathComponent("Bleam", isDirectory: true)
|
|
498
391
|
.appendingPathComponent("GenerationRuns", isDirectory: true)
|
|
499
392
|
.appendingPathComponent("bleam-generation-jobs", isDirectory: true)
|
|
500
393
|
.appendingPathComponent(jobID.uuidString, isDirectory: true)
|
|
@@ -627,31 +520,35 @@ public final class AIModule: Module {
|
|
|
627
520
|
|
|
628
521
|
#if canImport(FoundationModels)
|
|
629
522
|
@available(iOS 26, *)
|
|
630
|
-
private static func session(
|
|
631
|
-
|
|
632
|
-
|
|
523
|
+
private static func session(
|
|
524
|
+
system: String?,
|
|
525
|
+
messages: [TextMessageRequest]
|
|
526
|
+
) throws -> LanguageModelSession {
|
|
527
|
+
if !messages.isEmpty {
|
|
528
|
+
var entries: [Transcript.Entry] = []
|
|
529
|
+
if system?.isEmpty == false {
|
|
530
|
+
entries.append(.instructions(Transcript.Instructions(
|
|
531
|
+
segments: system.map { [.text(Transcript.TextSegment(content: $0))] } ?? [],
|
|
532
|
+
toolDefinitions: []
|
|
533
|
+
)))
|
|
534
|
+
}
|
|
535
|
+
for message in messages {
|
|
536
|
+
let segments: [Transcript.Segment] = [.text(Transcript.TextSegment(content: message.content))]
|
|
537
|
+
switch message.role {
|
|
538
|
+
case "user":
|
|
539
|
+
entries.append(.prompt(Transcript.Prompt(segments: segments)))
|
|
540
|
+
case "assistant":
|
|
541
|
+
entries.append(.response(Transcript.Response(assetIDs: [], segments: segments)))
|
|
542
|
+
default:
|
|
543
|
+
throw ModuleError.invalidMessage("Unsupported transcript role: \(message.role)")
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return LanguageModelSession(transcript: Transcript(entries: entries))
|
|
633
547
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
@available(iOS 26, *)
|
|
638
|
-
private static func tools(
|
|
639
|
-
from definitions: [ToolDefinitionRequest],
|
|
640
|
-
generationID: String,
|
|
641
|
-
coordinator: TextGenerationCoordinator,
|
|
642
|
-
emit: @escaping @MainActor @Sendable ([String: Any]) -> Void
|
|
643
|
-
) throws -> [any Tool] {
|
|
644
|
-
try definitions.map { definition in
|
|
645
|
-
let parameters = try generationSchema(from: definition.inputSchema)
|
|
646
|
-
return DynamicTool(
|
|
647
|
-
name: definition.name,
|
|
648
|
-
description: definition.description,
|
|
649
|
-
parameters: parameters,
|
|
650
|
-
generationID: generationID,
|
|
651
|
-
coordinator: coordinator,
|
|
652
|
-
emit: emit
|
|
653
|
-
)
|
|
548
|
+
if let system, !system.isEmpty {
|
|
549
|
+
return LanguageModelSession(instructions: system)
|
|
654
550
|
}
|
|
551
|
+
return LanguageModelSession()
|
|
655
552
|
}
|
|
656
553
|
|
|
657
554
|
@available(iOS 26, *)
|
|
@@ -663,131 +560,6 @@ public final class AIModule: Module {
|
|
|
663
560
|
)
|
|
664
561
|
}
|
|
665
562
|
|
|
666
|
-
@available(iOS 26, *)
|
|
667
|
-
private static func generationSchema(from schema: [String: Any]) throws -> GenerationSchema {
|
|
668
|
-
do {
|
|
669
|
-
return try GenerationSchema(root: dynamicSchema(from: schema), dependencies: [])
|
|
670
|
-
} catch {
|
|
671
|
-
throw ModuleError.invalidSchema(error.localizedDescription)
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
@available(iOS 26, *)
|
|
676
|
-
private static func dynamicSchema(from schema: [String: Any]) throws -> DynamicGenerationSchema {
|
|
677
|
-
guard let type = schema["type"] as? String else {
|
|
678
|
-
throw ModuleError.invalidSchema("Missing schema type")
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
switch type {
|
|
682
|
-
case "object":
|
|
683
|
-
return try objectSchema(from: schema)
|
|
684
|
-
case "array":
|
|
685
|
-
guard let itemSchema = schema["items"] as? [String: Any] else {
|
|
686
|
-
throw ModuleError.invalidSchema("Array schema must include items")
|
|
687
|
-
}
|
|
688
|
-
return try DynamicGenerationSchema(arrayOf: dynamicSchema(from: itemSchema))
|
|
689
|
-
case "string":
|
|
690
|
-
return DynamicGenerationSchema(type: String.self)
|
|
691
|
-
case "number":
|
|
692
|
-
return DynamicGenerationSchema(type: Double.self)
|
|
693
|
-
case "boolean":
|
|
694
|
-
return DynamicGenerationSchema(type: Bool.self)
|
|
695
|
-
default:
|
|
696
|
-
throw ModuleError.invalidSchema("Unsupported schema type: \(type)")
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
@available(iOS 26, *)
|
|
701
|
-
private static func objectSchema(from schema: [String: Any]) throws -> DynamicGenerationSchema {
|
|
702
|
-
let propertiesDictionary = schema["properties"] as? [String: Any] ?? [:]
|
|
703
|
-
let required = Set(schema["required"] as? [String] ?? [])
|
|
704
|
-
var properties: [DynamicGenerationSchema.Property] = []
|
|
705
|
-
|
|
706
|
-
for key in propertiesDictionary.keys.sorted() {
|
|
707
|
-
guard let propertySchema = propertiesDictionary[key] as? [String: Any] else {
|
|
708
|
-
throw ModuleError.invalidSchema("Property \(key) schema must be an object")
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
properties.append(
|
|
712
|
-
DynamicGenerationSchema.Property(
|
|
713
|
-
name: key,
|
|
714
|
-
description: nil,
|
|
715
|
-
schema: try dynamicSchema(from: propertySchema),
|
|
716
|
-
isOptional: !required.contains(key)
|
|
717
|
-
)
|
|
718
|
-
)
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
return DynamicGenerationSchema(name: schema["title"] as? String ?? "Output", properties: properties)
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
@available(iOS 26, *)
|
|
725
|
-
private static func value(from content: GeneratedContent, schema: [String: Any]) throws -> Any {
|
|
726
|
-
guard let type = schema["type"] as? String else {
|
|
727
|
-
throw ModuleError.invalidSchema("Missing schema type")
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
switch type {
|
|
731
|
-
case "object":
|
|
732
|
-
let propertiesDictionary = schema["properties"] as? [String: Any] ?? [:]
|
|
733
|
-
let output = NSMutableDictionary()
|
|
734
|
-
|
|
735
|
-
for key in propertiesDictionary.keys.sorted() {
|
|
736
|
-
guard let propertySchema = propertiesDictionary[key] as? [String: Any] else {
|
|
737
|
-
throw ModuleError.invalidSchema("Property \(key) schema must be an object")
|
|
738
|
-
}
|
|
739
|
-
output[key] = try propertyValue(from: content, property: key, schema: propertySchema)
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
return output
|
|
743
|
-
default:
|
|
744
|
-
throw ModuleError.invalidSchema("Root schema must be an object")
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
@available(iOS 26, *)
|
|
749
|
-
private static func propertyValue(
|
|
750
|
-
from content: GeneratedContent,
|
|
751
|
-
property: String,
|
|
752
|
-
schema: [String: Any]
|
|
753
|
-
) throws -> Any {
|
|
754
|
-
guard let type = schema["type"] as? String else {
|
|
755
|
-
throw ModuleError.invalidSchema("Missing schema type")
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
switch type {
|
|
759
|
-
case "string":
|
|
760
|
-
return try content.value(String.self, forProperty: property)
|
|
761
|
-
case "number":
|
|
762
|
-
return try content.value(Double.self, forProperty: property)
|
|
763
|
-
case "boolean":
|
|
764
|
-
return try content.value(Bool.self, forProperty: property)
|
|
765
|
-
case "object":
|
|
766
|
-
let nestedContent = try content.value(GeneratedContent.self, forProperty: property)
|
|
767
|
-
return try value(from: nestedContent, schema: schema)
|
|
768
|
-
case "array":
|
|
769
|
-
guard let itemSchema = schema["items"] as? [String: Any], let itemType = itemSchema["type"] as? String else {
|
|
770
|
-
throw ModuleError.invalidSchema("Array schema must include items")
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
switch itemType {
|
|
774
|
-
case "string":
|
|
775
|
-
return try content.value([String].self, forProperty: property)
|
|
776
|
-
case "number":
|
|
777
|
-
return try content.value([Double].self, forProperty: property)
|
|
778
|
-
case "boolean":
|
|
779
|
-
return try content.value([Bool].self, forProperty: property)
|
|
780
|
-
case "object":
|
|
781
|
-
let values = try content.value([GeneratedContent].self, forProperty: property)
|
|
782
|
-
return try values.map { try value(from: $0, schema: itemSchema) }
|
|
783
|
-
default:
|
|
784
|
-
throw ModuleError.invalidSchema("Unsupported array item type: \(itemType)")
|
|
785
|
-
}
|
|
786
|
-
default:
|
|
787
|
-
throw ModuleError.invalidSchema("Unsupported schema type: \(type)")
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
|
|
791
563
|
@available(iOS 26, *)
|
|
792
564
|
private static func reject(_ error: Error, promise: Promise) {
|
|
793
565
|
if error is CancellationError {
|
|
@@ -811,55 +583,14 @@ public final class AIModule: Module {
|
|
|
811
583
|
}
|
|
812
584
|
return
|
|
813
585
|
}
|
|
814
|
-
if let toolError = error as? LanguageModelSession.ToolCallError {
|
|
815
|
-
promise.reject("TOOL_ERROR", toolError.localizedDescription)
|
|
816
|
-
return
|
|
817
|
-
}
|
|
818
|
-
|
|
819
586
|
promise.reject(error)
|
|
820
587
|
}
|
|
821
588
|
#endif
|
|
822
589
|
}
|
|
823
590
|
|
|
824
|
-
#if canImport(FoundationModels)
|
|
825
|
-
@available(iOS 26, *)
|
|
826
|
-
private struct DynamicTool: Tool {
|
|
827
|
-
typealias Arguments = GeneratedContent
|
|
828
|
-
|
|
829
|
-
let name: String
|
|
830
|
-
let description: String
|
|
831
|
-
let parameters: GenerationSchema
|
|
832
|
-
let generationID: String
|
|
833
|
-
let coordinator: TextGenerationCoordinator
|
|
834
|
-
let emit: @MainActor @Sendable ([String: Any]) -> Void
|
|
835
|
-
|
|
836
|
-
func call(arguments: GeneratedContent) async throws -> String {
|
|
837
|
-
let data = Data(arguments.jsonString.utf8)
|
|
838
|
-
let value = try JSONSerialization.jsonObject(with: data)
|
|
839
|
-
return try await coordinator.call(
|
|
840
|
-
generationID: generationID,
|
|
841
|
-
name: name,
|
|
842
|
-
arguments: value,
|
|
843
|
-
emit: emit
|
|
844
|
-
)
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
#endif
|
|
848
|
-
|
|
849
|
-
private struct ToolExecutionError: LocalizedError {
|
|
850
|
-
let message: String
|
|
851
|
-
var errorDescription: String? { message }
|
|
852
|
-
}
|
|
853
|
-
|
|
854
591
|
@MainActor
|
|
855
592
|
private final class TextGenerationCoordinator {
|
|
856
|
-
private struct PendingToolCall {
|
|
857
|
-
let generationID: String
|
|
858
|
-
let continuation: CheckedContinuation<String, Error>
|
|
859
|
-
}
|
|
860
|
-
|
|
861
593
|
private var tasks: [String: Task<Void, Never>] = [:]
|
|
862
|
-
private var toolCalls: [String: PendingToolCall] = [:]
|
|
863
594
|
|
|
864
595
|
func start(_ id: String, task: Task<Void, Never>) {
|
|
865
596
|
tasks[id]?.cancel()
|
|
@@ -868,67 +599,22 @@ private final class TextGenerationCoordinator {
|
|
|
868
599
|
|
|
869
600
|
func finish(_ id: String) {
|
|
870
601
|
tasks[id] = nil
|
|
871
|
-
failToolCalls(for: id, error: CancellationError())
|
|
872
602
|
}
|
|
873
603
|
|
|
874
604
|
func cancel(_ id: String) -> Bool {
|
|
875
605
|
guard let task = tasks.removeValue(forKey: id) else { return false }
|
|
876
606
|
task.cancel()
|
|
877
|
-
failToolCalls(for: id, error: CancellationError())
|
|
878
607
|
return true
|
|
879
608
|
}
|
|
880
|
-
|
|
881
|
-
func call(
|
|
882
|
-
generationID: String,
|
|
883
|
-
name: String,
|
|
884
|
-
arguments: Any,
|
|
885
|
-
emit: @escaping @MainActor @Sendable ([String: Any]) -> Void
|
|
886
|
-
) async throws -> String {
|
|
887
|
-
try Task.checkCancellation()
|
|
888
|
-
let callID = UUID().uuidString
|
|
889
|
-
return try await withCheckedThrowingContinuation { continuation in
|
|
890
|
-
toolCalls[callID] = PendingToolCall(generationID: generationID, continuation: continuation)
|
|
891
|
-
emit([
|
|
892
|
-
"generationId": generationID,
|
|
893
|
-
"callId": callID,
|
|
894
|
-
"name": name,
|
|
895
|
-
"arguments": arguments,
|
|
896
|
-
])
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
func resolve(_ request: ToolResultRequest) -> Bool {
|
|
901
|
-
guard let pending = toolCalls.removeValue(forKey: request.callId),
|
|
902
|
-
pending.generationID == request.generationId
|
|
903
|
-
else { return false }
|
|
904
|
-
|
|
905
|
-
if let error = request.error {
|
|
906
|
-
pending.continuation.resume(throwing: ToolExecutionError(message: error))
|
|
907
|
-
} else {
|
|
908
|
-
pending.continuation.resume(returning: request.output ?? "")
|
|
909
|
-
}
|
|
910
|
-
return true
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
private func failToolCalls(for generationID: String, error: Error) {
|
|
914
|
-
let matches = toolCalls.filter { $0.value.generationID == generationID }
|
|
915
|
-
for (id, pending) in matches {
|
|
916
|
-
toolCalls[id] = nil
|
|
917
|
-
pending.continuation.resume(throwing: error)
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
609
|
}
|
|
921
610
|
|
|
922
611
|
private enum ModuleError: Error, LocalizedError {
|
|
923
|
-
case invalidSchema(String)
|
|
924
612
|
case invalidMessage(String)
|
|
925
613
|
case invalidModel(String)
|
|
926
614
|
case imageGeneration(String)
|
|
927
615
|
|
|
928
616
|
var errorDescription: String? {
|
|
929
617
|
switch self {
|
|
930
|
-
case .invalidSchema(let message):
|
|
931
|
-
return "Invalid schema: \(message)"
|
|
932
618
|
case .invalidMessage(let message), .invalidModel(let message), .imageGeneration(let message):
|
|
933
619
|
return message
|
|
934
620
|
}
|
|
@@ -1368,10 +1054,14 @@ private struct TextGenerationRequest: Record {
|
|
|
1368
1054
|
@Field var id: String?
|
|
1369
1055
|
@Field var system: String?
|
|
1370
1056
|
@Field var prompt: String = ""
|
|
1371
|
-
@Field var
|
|
1057
|
+
@Field var messages: [TextMessageRequest] = []
|
|
1372
1058
|
@Field var temperature: Double?
|
|
1373
1059
|
@Field var maxTokens: Int?
|
|
1374
|
-
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
private struct TextMessageRequest: Record {
|
|
1063
|
+
@Field var role: String = ""
|
|
1064
|
+
@Field var content: String = ""
|
|
1375
1065
|
}
|
|
1376
1066
|
|
|
1377
1067
|
private struct LocalTextGenerationRequest: Record {
|
|
@@ -1389,19 +1079,6 @@ private struct LocalTextGenerationRequest: Record {
|
|
|
1389
1079
|
@Field var seed: Int?
|
|
1390
1080
|
}
|
|
1391
1081
|
|
|
1392
|
-
private struct ToolDefinitionRequest: Record {
|
|
1393
|
-
@Field var name: String = ""
|
|
1394
|
-
@Field var description: String = ""
|
|
1395
|
-
@Field var inputSchema: [String: Any] = [:]
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
private struct ToolResultRequest: Record {
|
|
1399
|
-
@Field var generationId: String = ""
|
|
1400
|
-
@Field var callId: String = ""
|
|
1401
|
-
@Field var output: String?
|
|
1402
|
-
@Field var error: String?
|
|
1403
|
-
}
|
|
1404
|
-
|
|
1405
1082
|
private struct ImageGenerationRequest: Record {
|
|
1406
1083
|
@Field var model: String = ""
|
|
1407
1084
|
@Field var prompt: String = ""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { Button } from 'bleam/elements'
|
|
1
2
|
import { atom, useAtom } from 'bleam/state'
|
|
2
3
|
import { sx } from 'bleam/styles'
|
|
3
|
-
import { Button } from 'bleam/ui'
|
|
4
4
|
import { Window } from 'bleam/window'
|
|
5
5
|
import { Text, View } from 'react-native'
|
|
6
6
|
|
|
@@ -20,7 +20,7 @@ export default function App() {
|
|
|
20
20
|
</Text>
|
|
21
21
|
))}
|
|
22
22
|
<Button
|
|
23
|
-
|
|
23
|
+
label="Add task"
|
|
24
24
|
onPress={() =>
|
|
25
25
|
setItems((current) => [...current, `Task ${current.length + 1}`])
|
|
26
26
|
}
|
|
@@ -1,55 +1,48 @@
|
|
|
1
|
-
import { bonsai, prepareTextModel,
|
|
1
|
+
import { bonsai, chat, prepareTextModel, useChat, useChatList } from 'bleam/ai'
|
|
2
|
+
import { Button } from 'bleam/elements'
|
|
2
3
|
import { sx } from 'bleam/styles'
|
|
3
|
-
import { Button } from 'bleam/ui'
|
|
4
4
|
import { Window } from 'bleam/window'
|
|
5
|
-
import { useRef, useState } from 'react'
|
|
5
|
+
import { useEffect, useRef, useState } from 'react'
|
|
6
6
|
import { Text, TextInput, View } from 'react-native'
|
|
7
7
|
|
|
8
8
|
const appName = 'Bleam Basic'
|
|
9
9
|
const model = bonsai('ternary-bonsai-4b')
|
|
10
10
|
|
|
11
11
|
export default function App() {
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
)
|
|
15
|
-
const [
|
|
16
|
-
const [status, setStatus] = useState('Ready')
|
|
17
|
-
const [isRunning, setIsRunning] = useState(false)
|
|
18
|
-
const controller = useRef<AbortController>(null)
|
|
12
|
+
const chats = useChatList()
|
|
13
|
+
const creating = useRef(false)
|
|
14
|
+
const [path, setPath] = useState<string | null>(null)
|
|
15
|
+
const [status, setStatus] = useState('Preparing local model...')
|
|
19
16
|
|
|
20
|
-
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (chats.length > 0 || creating.current) return
|
|
19
|
+
creating.current = true
|
|
20
|
+
void chat.create({ title: 'Private writing' }).catch((error) => {
|
|
21
|
+
setStatus(error instanceof Error ? error.message : String(error))
|
|
22
|
+
})
|
|
23
|
+
}, [chats.length])
|
|
26
24
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
)
|
|
34
|
-
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const controller = new AbortController()
|
|
27
|
+
void prepareTextModel(model, {
|
|
28
|
+
signal: controller.signal,
|
|
29
|
+
onProgress(progress) {
|
|
30
|
+
setStatus(
|
|
31
|
+
`Preparing model ${Math.round(progress.modelFraction * 100)}%`,
|
|
32
|
+
)
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
.then((prepared) => {
|
|
36
|
+
setPath(prepared.path)
|
|
37
|
+
setStatus('Ready')
|
|
35
38
|
})
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
setAnswer(text)
|
|
44
|
-
}
|
|
45
|
-
setStatus('Complete')
|
|
46
|
-
} catch (error) {
|
|
47
|
-
setStatus(error instanceof Error ? error.message : String(error))
|
|
48
|
-
} finally {
|
|
49
|
-
if (controller.current === nextController) controller.current = null
|
|
50
|
-
setIsRunning(false)
|
|
51
|
-
}
|
|
52
|
-
}
|
|
39
|
+
.catch((error) => {
|
|
40
|
+
if (!controller.signal.aborted) {
|
|
41
|
+
setStatus(error instanceof Error ? error.message : String(error))
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
return () => controller.abort()
|
|
45
|
+
}, [])
|
|
53
46
|
|
|
54
47
|
return (
|
|
55
48
|
<View style={sx('flex-1', 'p-8', 'gap-6')}>
|
|
@@ -58,14 +51,41 @@ export default function App() {
|
|
|
58
51
|
Private Writing Assistant
|
|
59
52
|
</Text>
|
|
60
53
|
<Text style={sx('text-secondary-label')}>
|
|
61
|
-
Ternary Bonsai runs locally after its first model download.
|
|
54
|
+
Ternary Bonsai runs locally after its first model download. Chats return
|
|
55
|
+
after relaunch.
|
|
62
56
|
</Text>
|
|
57
|
+
<Text style={sx('text-secondary-label')}>{status}</Text>
|
|
58
|
+
{path && chats[0] ? <Conversation id={chats[0].id} path={path} /> : null}
|
|
59
|
+
</View>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function Conversation({ id, path }: { id: string; path: string }) {
|
|
64
|
+
const conversation = useChat(id, {
|
|
65
|
+
model,
|
|
66
|
+
path,
|
|
67
|
+
system: 'You are a concise private writing assistant.',
|
|
68
|
+
})
|
|
69
|
+
const [prompt, setPrompt] = useState(
|
|
70
|
+
'Rewrite this note so it is clear and concise: We should probably meet sometime next week to discuss the launch.',
|
|
71
|
+
)
|
|
72
|
+
const latestAssistant = conversation.messages.findLast(
|
|
73
|
+
(message) => message.role === 'assistant',
|
|
74
|
+
)
|
|
75
|
+
const retryable =
|
|
76
|
+
latestAssistant?.status === 'failed' ||
|
|
77
|
+
latestAssistant?.status === 'canceled'
|
|
78
|
+
? latestAssistant
|
|
79
|
+
: null
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<View style={sx('gap-6')}>
|
|
63
83
|
<TextInput
|
|
64
84
|
multiline
|
|
65
85
|
value={prompt}
|
|
66
86
|
onChangeText={setPrompt}
|
|
67
87
|
style={sx(
|
|
68
|
-
|
|
88
|
+
{ minHeight: 128 },
|
|
69
89
|
'rounded-xl',
|
|
70
90
|
'border',
|
|
71
91
|
'border-separator',
|
|
@@ -74,20 +94,31 @@ export default function App() {
|
|
|
74
94
|
/>
|
|
75
95
|
<View style={sx('flex-row', 'gap-3')}>
|
|
76
96
|
<Button
|
|
77
|
-
|
|
78
|
-
loading={
|
|
79
|
-
disabled={
|
|
80
|
-
onPress={() => void
|
|
97
|
+
label="Send"
|
|
98
|
+
loading={conversation.isGenerating}
|
|
99
|
+
disabled={!conversation.canSend}
|
|
100
|
+
onPress={() => void conversation.send(prompt)}
|
|
81
101
|
/>
|
|
82
102
|
<Button
|
|
83
|
-
|
|
103
|
+
label="Cancel"
|
|
84
104
|
variant="secondary"
|
|
85
|
-
disabled={!
|
|
86
|
-
onPress={() =>
|
|
105
|
+
disabled={!conversation.canCancel}
|
|
106
|
+
onPress={() => void conversation.cancel()}
|
|
87
107
|
/>
|
|
108
|
+
{retryable ? (
|
|
109
|
+
<Button
|
|
110
|
+
label="Retry"
|
|
111
|
+
variant="secondary"
|
|
112
|
+
onPress={() => void conversation.retry(retryable.id)}
|
|
113
|
+
/>
|
|
114
|
+
) : null}
|
|
88
115
|
</View>
|
|
89
|
-
|
|
90
|
-
|
|
116
|
+
{conversation.messages.map((message) => (
|
|
117
|
+
<Text key={message.id} style={sx('text-lg', 'text-label')}>
|
|
118
|
+
{message.role === 'user' ? 'You: ' : 'Assistant: '}
|
|
119
|
+
{message.content || message.status}
|
|
120
|
+
</Text>
|
|
121
|
+
))}
|
|
91
122
|
</View>
|
|
92
123
|
)
|
|
93
124
|
}
|