History.swift 2.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. // Copyright 2025 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. import Foundation
  15. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
  16. final class History: Sendable {
  17. private let historyLock = NSLock()
  18. private nonisolated(unsafe) var _history: [ModelContent] = []
  19. /// The previous content from the chat that has been successfully sent and received from the
  20. /// model. This will be provided to the model for each message sent as context for the discussion.
  21. public var history: [ModelContent] {
  22. get {
  23. historyLock.withLock { _history }
  24. }
  25. set {
  26. historyLock.withLock { _history = newValue }
  27. }
  28. }
  29. init(history: [ModelContent]) {
  30. self.history = history
  31. }
  32. func append(contentsOf: [ModelContent]) {
  33. historyLock.withLock {
  34. _history.append(contentsOf: contentsOf)
  35. }
  36. }
  37. func append(_ newElement: ModelContent) {
  38. historyLock.withLock {
  39. _history.append(newElement)
  40. }
  41. }
  42. func aggregatedChunks(_ chunks: [ModelContent]) -> ModelContent {
  43. var parts: [InternalPart] = []
  44. var combinedText = ""
  45. var combinedThoughts = ""
  46. func flush() {
  47. if !combinedThoughts.isEmpty {
  48. parts.append(InternalPart(.text(combinedThoughts), isThought: true, thoughtSignature: nil))
  49. combinedThoughts = ""
  50. }
  51. if !combinedText.isEmpty {
  52. parts.append(InternalPart(.text(combinedText), isThought: nil, thoughtSignature: nil))
  53. combinedText = ""
  54. }
  55. }
  56. // Loop through all the parts, aggregating the text.
  57. for part in chunks.flatMap({ $0.internalParts }) {
  58. // Only text parts may be combined.
  59. if case let .text(text) = part.data, part.thoughtSignature == nil {
  60. // Thought summaries must not be combined with regular text.
  61. if part.isThought ?? false {
  62. // If we were combining regular text, flush it before handling "thoughts".
  63. if !combinedText.isEmpty {
  64. flush()
  65. }
  66. combinedThoughts += text
  67. } else {
  68. // If we were combining "thoughts", flush it before handling regular text.
  69. if !combinedThoughts.isEmpty {
  70. flush()
  71. }
  72. combinedText += text
  73. }
  74. } else {
  75. // This is a non-combinable part (not text), flush any pending text.
  76. flush()
  77. parts.append(part)
  78. }
  79. }
  80. // Flush any remaining text.
  81. flush()
  82. return ModelContent(role: "model", parts: parts)
  83. }
  84. }