Skip to content
Open

1626 #25

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 46 additions & 2 deletions .github/workflows/linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,25 @@ jobs:
- name: Build
run: swift build -v

- name: Test
run: swift test -v
- name: Test with Coverage
run: swift test --enable-code-coverage -v

- name: Generate Coverage Report
run: |
BIN_PATH=$(swift build --show-bin-path)
XCTEST_PATH=$(find "$BIN_PATH" -name "*.xctest" -type f | head -1)
PROFDATA=$(find .build -name "default.profdata" | head -1)
if [ -n "$XCTEST_PATH" ] && [ -n "$PROFDATA" ]; then
llvm-cov export -format=lcov \
-instr-profile="$PROFDATA" "$XCTEST_PATH" \
-ignore-filename-regex='.build|Tests' > coverage.lcov
fi

- name: Upload Coverage
uses: codecov/codecov-action@v4
with:
files: coverage.lcov
fail_ci_if_error: false

test-macros:
name: Verify Macros on Linux
Expand Down Expand Up @@ -77,3 +94,30 @@ jobs:

- name: Test Macros
run: swift test --filter ConduitMacrosTests

test-with-providers:
name: Test with Provider Traits
runs-on: ubuntu-latest
timeout-minutes: 20

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Swift
uses: vapor/[email protected]
with:
toolchain: "6.2"

- name: Cache SPM
uses: actions/cache@v4
with:
path: |
~/.cache/org.swift.swiftpm
.build
key: linux-swift-6.2-spm-providers-${{ hashFiles('**/Package.resolved') }}
restore-keys: |
linux-swift-6.2-spm-providers-

- name: Test with traits
run: swift test --traits OpenAI,Anthropic,Kimi,MiniMax -v
20 changes: 20 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,18 @@ let package = Package(
.package(url: "https://github.com/mattt/llama.swift", .upToNextMajor(from: "2.7484.0")),
],
targets: [
.target(
name: "ConduitCore",
dependencies: [],
path: "Sources/ConduitCore",
publicHeadersPath: "include",
cSettings: [
.define("CONDUIT_HAS_ACCELERATE", .when(platforms: [.macOS, .iOS, .visionOS, .tvOS, .watchOS])),
],
linkerSettings: [
.linkedFramework("Accelerate", .when(platforms: [.macOS, .iOS, .visionOS, .tvOS, .watchOS])),
]
),
.macro(
name: "ConduitMacros",
dependencies: [
Expand All @@ -86,6 +98,7 @@ let package = Package(
.target(
name: "Conduit",
dependencies: [
"ConduitCore",
"ConduitMacros",
.product(name: "OrderedCollections", package: "swift-collections"),
.product(name: "Logging", package: "swift-log"),
Expand Down Expand Up @@ -127,6 +140,13 @@ let package = Package(
.enableExperimentalFeature("StrictConcurrency")
]
),
.testTarget(
name: "ConduitCoreTests",
dependencies: [
"ConduitCore",
],
path: "Tests/ConduitCoreTests"
),
.testTarget(
name: "ConduitMacrosTests",
dependencies: [
Expand Down
141 changes: 141 additions & 0 deletions Sources/Conduit/ChatSession+History.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// ChatSession+History.swift
// Conduit

import Foundation

// MARK: - History Management

extension ChatSession {

/// Clears all messages except the system prompt.
///
/// If a system message exists at the beginning of the history,
/// it is preserved. All other messages are removed.
///
/// ## Usage
///
/// ```swift
/// session.clearHistory()
/// // System prompt is preserved, conversation is reset
/// ```
public func clearHistory() {
withLock {
if let systemMessage = messages.first, systemMessage.role == .system {
messages = [systemMessage]
} else {
messages = []
}
}
}

/// Removes the last user-assistant exchange from history.
///
/// This removes the most recent pair of user and assistant messages,
/// allowing you to "undo" the last conversation turn.
///
/// If the last message is a user message without a response, only
/// that user message is removed.
///
/// ## Usage
///
/// ```swift
/// // After an unsatisfactory response
/// session.undoLastExchange()
/// // Try again with different phrasing
/// let response = try await session.send("Let me rephrase...")
/// ```
public func undoLastExchange() {
withLock {
guard !messages.isEmpty else { return }

// Remove assistant message if it's the last one
if messages.last?.role == .assistant {
messages.removeLast()
}

// Remove user message if it's now the last one
if messages.last?.role == .user {
messages.removeLast()
}
}
}

/// Injects a conversation history, preserving the current system prompt.
///
/// If the current session has a system prompt, it is preserved and
/// the injected history (minus any system messages) is appended.
///
/// If the current session has no system prompt but the injected
/// history has one, that system prompt is used.
///
/// ## Usage
///
/// ```swift
/// // Load saved conversation
/// let savedMessages = loadMessagesFromDisk()
/// session.injectHistory(savedMessages)
/// ```
///
/// - Parameter history: The messages to inject.
public func injectHistory(_ history: [Message]) {
withLock {
// Check for existing system prompt
let existingSystemPrompt: Message? = messages.first?.role == .system
? messages.first
: nil

// Filter out system messages from injected history
let nonSystemMessages = history.filter { $0.role != .system }

// Check for system prompt in injected history
let injectedSystemPrompt = history.first { $0.role == .system }

// Build new message list
if let existingPrompt = existingSystemPrompt {
// Keep existing system prompt
messages = [existingPrompt] + nonSystemMessages
} else if let injectedPrompt = injectedSystemPrompt {
// Use injected system prompt
messages = [injectedPrompt] + nonSystemMessages
} else {
// No system prompt
messages = nonSystemMessages
}
}
}

// MARK: - Computed Properties

/// The total number of messages in the conversation.
///
/// Includes system, user, and assistant messages.
public var messageCount: Int {
withLock { messages.count }
}

/// The number of user messages in the conversation.
///
/// Useful for tracking the number of conversation turns.
public var userMessageCount: Int {
withLock {
messages.filter { $0.role == .user }.count
}
}

/// Whether the session has an active system prompt.
public var hasSystemPrompt: Bool {
withLock {
messages.first?.role == .system
}
}

/// The current system prompt, if any.
public var systemPrompt: String? {
withLock {
guard let first = messages.first, first.role == .system else {
return nil
}
return first.content.textValue
}
}
}
Loading
Loading