@effect/ai 0.28.2 → 0.28.3

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/src/Chat.ts CHANGED
@@ -54,11 +54,13 @@ import * as Effect from "effect/Effect"
54
54
  import * as Layer from "effect/Layer"
55
55
  import * as Option from "effect/Option"
56
56
  import type { ParseError } from "effect/ParseResult"
57
+ import * as Predicate from "effect/Predicate"
57
58
  import * as Ref from "effect/Ref"
58
59
  import * as Schema from "effect/Schema"
59
60
  import * as Stream from "effect/Stream"
60
61
  import type { NoExcessProperties } from "effect/Types"
61
62
  import * as AiError from "./AiError.js"
63
+ import * as IdGenerator from "./IdGenerator.js"
62
64
  import * as LanguageModel from "./LanguageModel.js"
63
65
  import * as Prompt from "./Prompt.js"
64
66
  import type * as Response from "./Response.js"
@@ -651,6 +653,11 @@ export interface Persisted extends Service {
651
653
  * The identifier for the chat in the backing persistence store.
652
654
  */
653
655
  readonly id: string
656
+
657
+ /**
658
+ * Saves the current chat history into the backing persistence store.
659
+ */
660
+ readonly save: Effect.Effect<void, AiError.MalformedOutput | PersistenceBackingError>
654
661
  }
655
662
 
656
663
  /**
@@ -668,28 +675,77 @@ export const makePersisted = Effect.fnUntraced(function*(options: {
668
675
  const persistence = yield* BackingPersistence
669
676
  const store = yield* persistence.make(options.storeId)
670
677
 
671
- const toPersisted = (chatId: string, chat: Service): Persisted => {
672
- const persistChat = chat.exportJson.pipe(
673
- Effect.flatMap((history) => store.set(chatId, history, Option.none())),
674
- Effect.orDie
678
+ const toPersisted = Effect.fnUntraced(function*(chatId: string, chat: Service) {
679
+ const idGenerator = yield* Effect.serviceOption(IdGenerator.IdGenerator).pipe(
680
+ Effect.map(Option.getOrElse(() => IdGenerator.defaultIdGenerator))
675
681
  )
676
- return {
682
+
683
+ const saveChat = Effect.fnUntraced(
684
+ function*(prevHistory: Prompt.Prompt) {
685
+ // Get the current chat history
686
+ const history = yield* Ref.get(chat.history)
687
+ // Get the most recent message stored in the previous chat history
688
+ const lastMessage = prevHistory.content[prevHistory.content.length - 1]
689
+ // Determine the correct message identifier to use:
690
+ let messageId: string | undefined = undefined
691
+ // If the most recent message in the chat history is an assistant message,
692
+ // use the message identifer stored in that message
693
+ if (Predicate.isNotUndefined(lastMessage) && lastMessage.role === "assistant") {
694
+ messageId = lastMessage.options[Persistence.key]?.messageId as string | undefined
695
+ }
696
+ // If the chat history is empty or a message identifier did not exist on
697
+ // the most recent message in the chat history, generate a new identifier
698
+ if (Predicate.isUndefined(messageId)) {
699
+ messageId = yield* idGenerator.generateId()
700
+ }
701
+ // Mutate the new messages to add the generated message identifier
702
+ for (let i = prevHistory.content.length; i < history.content.length; i++) {
703
+ const message = history.content[i]
704
+ ;(message.options as any)[Persistence.key] = { messageId }
705
+ }
706
+ // Save the mutated history back to the ref
707
+ yield* Ref.set(chat.history, history)
708
+ // Export the chat to JSON
709
+ const json = yield* chat.exportJson
710
+ // Save the chat to the backing store
711
+ yield* store.set(chatId, json, Option.none())
712
+ },
713
+ Effect.catchTag("PersistenceError", (error) => {
714
+ // Should never happen because we are using the backing persistence
715
+ // store directly, and parse errors can only occur when using result
716
+ // persistence
717
+ if (error.reason === "ParseError") return Effect.die(error)
718
+ return Effect.fail(error)
719
+ })
720
+ )
721
+
722
+ const persisted: Persisted = {
677
723
  ...chat,
678
724
  id: chatId,
679
- generateText: (options) =>
680
- chat.generateText(options).pipe(
681
- Effect.ensuring(persistChat)
682
- ),
683
- generateObject: (options) =>
684
- chat.generateObject(options).pipe(
685
- Effect.ensuring(persistChat)
686
- ),
687
- streamText: (options) =>
688
- chat.streamText(options).pipe(
689
- Stream.ensuring(persistChat)
725
+ save: Effect.flatMap(Ref.get(chat.history), saveChat),
726
+ generateText: Effect.fnUntraced(function*(options) {
727
+ const history = yield* Ref.get(chat.history)
728
+ return yield* chat.generateText(options).pipe(
729
+ Effect.ensuring(Effect.orDie(saveChat(history)))
730
+ )
731
+ }),
732
+ generateObject: Effect.fnUntraced(function*(options) {
733
+ const history = yield* Ref.get(chat.history)
734
+ return yield* chat.generateObject(options).pipe(
735
+ Effect.ensuring(Effect.orDie(saveChat(history)))
690
736
  )
737
+ }),
738
+ streamText: Effect.fnUntraced(function*(options) {
739
+ const history = yield* Ref.get(chat.history)
740
+ const stream = chat.streamText(options).pipe(
741
+ Stream.ensuring(Effect.orDie(saveChat(history)))
742
+ )
743
+ return stream
744
+ }, Stream.unwrap)
691
745
  }
692
- }
746
+
747
+ return persisted
748
+ })
693
749
 
694
750
  const createChat = Effect.fnUntraced(
695
751
  function*(chatId: string) {
@@ -701,7 +757,7 @@ export const makePersisted = Effect.fnUntraced(function*(options: {
701
757
  yield* store.set(chatId, history, Option.none())
702
758
 
703
759
  // Convert the chat to a persisted chat
704
- return toPersisted(chatId, chat)
760
+ return yield* toPersisted(chatId, chat)
705
761
  },
706
762
  Effect.catchTag("PersistenceError", (error) => {
707
763
  // Should never happen because we are using the backing persistence
@@ -726,7 +782,7 @@ export const makePersisted = Effect.fnUntraced(function*(options: {
726
782
  )
727
783
 
728
784
  // Convert the chat to a persisted chat
729
- return toPersisted(chatId, chat)
785
+ return yield* toPersisted(chatId, chat)
730
786
  },
731
787
  Effect.catchTags({
732
788
  ParseError: (error) => Effect.die(error),
package/src/Prompt.ts CHANGED
@@ -51,6 +51,7 @@
51
51
  *
52
52
  * @since 1.0.0
53
53
  */
54
+ import * as Arbitrary from "effect/Arbitrary"
54
55
  import * as Arr from "effect/Array"
55
56
  import { constFalse, dual } from "effect/Function"
56
57
  import * as ParseResult from "effect/ParseResult"
@@ -225,10 +226,10 @@ export const makePart = <const Type extends Part["type"]>(
225
226
  }
226
227
  ): Extract<Part, { type: Type }> =>
227
228
  (({
228
- ...params,
229
- [PartTypeId]: PartTypeId,
230
- type,
231
- options: params.options ?? {}
229
+ ...params,
230
+ [PartTypeId]: PartTypeId,
231
+ type,
232
+ options: params.options ?? {}
232
233
  }) as any)
233
234
 
234
235
  // =============================================================================
@@ -724,10 +725,10 @@ export const makeMessage = <const Role extends Message["role"]>(
724
725
  }
725
726
  ): Extract<Message, { role: Role }> =>
726
727
  (({
727
- ...params,
728
- [MessageTypeId]: MessageTypeId,
729
- role,
730
- options: params.options ?? {}
728
+ ...params,
729
+ [MessageTypeId]: MessageTypeId,
730
+ role,
731
+ options: params.options ?? {}
731
732
  }) as any)
732
733
 
733
734
  /**
@@ -1214,7 +1215,11 @@ export class PromptFromSelf extends Schema.declare(
1214
1215
  (u) => isPrompt(u),
1215
1216
  {
1216
1217
  identifier: "PromptFromSelf",
1217
- description: "a Prompt instance"
1218
+ description: "a Prompt instance",
1219
+ arbitrary: (): Arbitrary.LazyArbitrary<Prompt> => (fc) =>
1220
+ fc.array(
1221
+ Arbitrary.makeLazy(Message)(fc)
1222
+ ).map(makePrompt)
1218
1223
  }
1219
1224
  ) {}
1220
1225
 
@@ -1467,14 +1472,15 @@ export const fromResponseParts = (parts: ReadonlyArray<Response.AnyPart>): Promp
1467
1472
  return empty
1468
1473
  }
1469
1474
 
1470
- const content: Array<AssistantMessagePart> = []
1475
+ const assistantParts: Array<AssistantMessagePart> = []
1476
+ const toolParts: Array<ToolMessagePart> = []
1471
1477
 
1472
1478
  const textDeltas: Array<string> = []
1473
1479
  function flushTextDeltas() {
1474
1480
  if (textDeltas.length > 0) {
1475
1481
  const text = textDeltas.join("")
1476
1482
  if (text.length > 0) {
1477
- content.push(makePart("text", { text }))
1483
+ assistantParts.push(makePart("text", { text }))
1478
1484
  }
1479
1485
  textDeltas.length = 0
1480
1486
  }
@@ -1485,7 +1491,7 @@ export const fromResponseParts = (parts: ReadonlyArray<Response.AnyPart>): Promp
1485
1491
  if (reasoningDeltas.length > 0) {
1486
1492
  const text = reasoningDeltas.join("")
1487
1493
  if (text.length > 0) {
1488
- content.push(makePart("reasoning", { text }))
1494
+ assistantParts.push(makePart("reasoning", { text }))
1489
1495
  }
1490
1496
  reasoningDeltas.length = 0
1491
1497
  }
@@ -1501,7 +1507,7 @@ export const fromResponseParts = (parts: ReadonlyArray<Response.AnyPart>): Promp
1501
1507
  switch (part.type) {
1502
1508
  case "text": {
1503
1509
  flushDeltas()
1504
- content.push(makePart("text", { text: part.text }))
1510
+ assistantParts.push(makePart("text", { text: part.text }))
1505
1511
  break
1506
1512
  }
1507
1513
  case "text-delta": {
@@ -1511,7 +1517,7 @@ export const fromResponseParts = (parts: ReadonlyArray<Response.AnyPart>): Promp
1511
1517
  }
1512
1518
  case "reasoning": {
1513
1519
  flushDeltas()
1514
- content.push(makePart("reasoning", { text: part.text }))
1520
+ assistantParts.push(makePart("reasoning", { text: part.text }))
1515
1521
  break
1516
1522
  }
1517
1523
  case "reasoning-delta": {
@@ -1521,7 +1527,7 @@ export const fromResponseParts = (parts: ReadonlyArray<Response.AnyPart>): Promp
1521
1527
  }
1522
1528
  case "tool-call": {
1523
1529
  flushDeltas()
1524
- content.push(makePart("tool-call", {
1530
+ assistantParts.push(makePart("tool-call", {
1525
1531
  id: part.id,
1526
1532
  name: part.providerName ?? part.name,
1527
1533
  params: part.params,
@@ -1531,7 +1537,7 @@ export const fromResponseParts = (parts: ReadonlyArray<Response.AnyPart>): Promp
1531
1537
  }
1532
1538
  case "tool-result": {
1533
1539
  flushDeltas()
1534
- content.push(makePart("tool-result", {
1540
+ toolParts.push(makePart("tool-result", {
1535
1541
  id: part.id,
1536
1542
  name: part.providerName ?? part.name,
1537
1543
  result: part.encodedResult
@@ -1544,9 +1550,18 @@ export const fromResponseParts = (parts: ReadonlyArray<Response.AnyPart>): Promp
1544
1550
 
1545
1551
  flushDeltas()
1546
1552
 
1547
- const message = makeMessage("assistant", { content })
1553
+ if (assistantParts.length === 0 && toolParts.length === 0) {
1554
+ return empty
1555
+ }
1548
1556
 
1549
- return makePrompt([message])
1557
+ const messages: Array<Message> = []
1558
+ if (assistantParts.length > 0) {
1559
+ messages.push(makeMessage("assistant", { content: assistantParts }))
1560
+ }
1561
+ if (toolParts.length > 0) {
1562
+ messages.push(makeMessage("tool", { content: toolParts }))
1563
+ }
1564
+ return makePrompt(messages)
1550
1565
  }
1551
1566
 
1552
1567
  // =============================================================================
@@ -1600,7 +1615,7 @@ export const merge: {
1600
1615
  * @since 1.0.0
1601
1616
  * @category Combinators
1602
1617
  */
1603
- (other: RawInput): (self: Prompt) => Prompt
1618
+ (input: RawInput): (self: Prompt) => Prompt
1604
1619
  // =============================================================================
1605
1620
  // Merging Prompts
1606
1621
  // =============================================================================
@@ -1626,12 +1641,17 @@ export const merge: {
1626
1641
  * @since 1.0.0
1627
1642
  * @category Combinators
1628
1643
  */
1629
- (self: Prompt, other: RawInput): Prompt
1630
- } = dual(2, (self: Prompt, other: RawInput): Prompt =>
1631
- fromMessages([
1632
- ...self.content,
1633
- ...make(other).content
1634
- ]))
1644
+ (self: Prompt, input: RawInput): Prompt
1645
+ } = dual(2, (self: Prompt, input: RawInput): Prompt => {
1646
+ const other = make(input)
1647
+ if (self.content.length === 0) {
1648
+ return other
1649
+ }
1650
+ if (other.content.length === 0) {
1651
+ return self
1652
+ }
1653
+ return fromMessages([...self.content, ...other.content])
1654
+ })
1635
1655
 
1636
1656
  // =============================================================================
1637
1657
  // Manipulating Prompts
package/src/Response.ts CHANGED
@@ -542,7 +542,7 @@ export const makePart = <const Type extends AnyPart["type"]>(
542
542
  ...params,
543
543
  [PartTypeId]: PartTypeId,
544
544
  type,
545
- metadata: params.metadata
545
+ metadata: params.metadata ?? {}
546
546
  }) as any)
547
547
 
548
548
  // =============================================================================