Implement optional discussion mode
This commit is contained in:
@@ -38,6 +38,11 @@ struct ProjectInspectorView: View {
|
||||
TextField("Title", text: $project.title)
|
||||
TextField("Idea", text: $project.idea, axis: .vertical)
|
||||
.lineLimit(2...4)
|
||||
Picker("AI mode", selection: $project.conversationMode) {
|
||||
Text("Auto").tag(ConversationMode.auto)
|
||||
Text("Discuss").tag(ConversationMode.discuss)
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
TextField("Duration seconds", text: durationSecondsBinding)
|
||||
TextField("Duration note", text: durationDescriptionBinding)
|
||||
}
|
||||
|
||||
@@ -49,6 +49,13 @@ public protocol OpenAIClientAdapter: Sendable {
|
||||
|
||||
func decodeProjectGenerationResult(from data: Data) throws -> SongProjectGenerationResult
|
||||
|
||||
func makeSongProjectDiscussionRequest(
|
||||
_ request: SongProjectDiscussionRequest,
|
||||
configuration: OpenAIClientConfiguration
|
||||
) throws -> OpenAIClientRequest
|
||||
|
||||
func decodeSongProjectDiscussionResult(from data: Data) throws -> SongProjectDiscussionResult
|
||||
|
||||
func makeLyricsRevisionRequest(
|
||||
_ request: LyricsRevisionRequest,
|
||||
configuration: OpenAIClientConfiguration
|
||||
@@ -99,6 +106,12 @@ public final class OpenAIClient: AIService, Sendable {
|
||||
return try adapter.decodeProjectGenerationResult(from: data)
|
||||
}
|
||||
|
||||
public func discussSongProject(from request: SongProjectDiscussionRequest) async throws -> SongProjectDiscussionResult {
|
||||
let clientRequest = try adapter.makeSongProjectDiscussionRequest(request, configuration: configuration)
|
||||
let data = try await perform(clientRequest)
|
||||
return try adapter.decodeSongProjectDiscussionResult(from: data)
|
||||
}
|
||||
|
||||
public func reviseLyrics(from request: LyricsRevisionRequest) async throws -> LyricsRevisionResult {
|
||||
let clientRequest = try adapter.makeLyricsRevisionRequest(request, configuration: configuration)
|
||||
let data = try await perform(clientRequest)
|
||||
|
||||
@@ -2,10 +2,21 @@ import Foundation
|
||||
|
||||
public protocol AIService: Sendable {
|
||||
func generateSongProject(from request: SongProjectGenerationRequest) async throws -> SongProjectGenerationResult
|
||||
func discussSongProject(from request: SongProjectDiscussionRequest) async throws -> SongProjectDiscussionResult
|
||||
func reviseLyrics(from request: LyricsRevisionRequest) async throws -> LyricsRevisionResult
|
||||
func proposeProjectUpdate(from request: SongProjectUpdateRequest) async throws -> SongProjectUpdateResult
|
||||
}
|
||||
|
||||
public extension AIService {
|
||||
func discussSongProject(from request: SongProjectDiscussionRequest) async throws -> SongProjectDiscussionResult {
|
||||
throw AIServiceCapabilityError.discussionNotSupported
|
||||
}
|
||||
}
|
||||
|
||||
public enum AIServiceCapabilityError: Error, Equatable, Sendable {
|
||||
case discussionNotSupported
|
||||
}
|
||||
|
||||
public struct AIConversationMessage: Equatable, Identifiable, Sendable {
|
||||
public let id: String
|
||||
public var role: AIConversationRole
|
||||
@@ -82,6 +93,26 @@ public struct SongProjectGenerationResult: Equatable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct SongProjectDiscussionRequest: Equatable, Sendable {
|
||||
public var context: AIRequestContext
|
||||
public var project: SongProject
|
||||
|
||||
public init(context: AIRequestContext, project: SongProject) {
|
||||
self.context = context
|
||||
self.project = project
|
||||
}
|
||||
}
|
||||
|
||||
public struct SongProjectDiscussionResult: Equatable, Sendable {
|
||||
public var questions: [String]
|
||||
public var notes: [String]
|
||||
|
||||
public init(questions: [String], notes: [String] = []) {
|
||||
self.questions = questions
|
||||
self.notes = notes
|
||||
}
|
||||
}
|
||||
|
||||
public struct LyricsRevisionRequest: Equatable, Sendable {
|
||||
public var context: AIRequestContext
|
||||
public var project: SongProject
|
||||
|
||||
@@ -45,6 +45,10 @@ public final class ApplicationRuleInjectingAIService: AIService, Sendable {
|
||||
try await baseService.generateSongProject(from: requestWithInjectedRules(request))
|
||||
}
|
||||
|
||||
public func discussSongProject(from request: SongProjectDiscussionRequest) async throws -> SongProjectDiscussionResult {
|
||||
try await baseService.discussSongProject(from: requestWithInjectedRules(request))
|
||||
}
|
||||
|
||||
public func reviseLyrics(from request: LyricsRevisionRequest) async throws -> LyricsRevisionResult {
|
||||
try await baseService.reviseLyrics(from: requestWithInjectedRules(request))
|
||||
}
|
||||
@@ -59,6 +63,12 @@ public final class ApplicationRuleInjectingAIService: AIService, Sendable {
|
||||
return request
|
||||
}
|
||||
|
||||
private func requestWithInjectedRules(_ request: SongProjectDiscussionRequest) throws -> SongProjectDiscussionRequest {
|
||||
var request = request
|
||||
request.context.injectPrivateApplicationRules(try ruleProvider.privateApplicationRules())
|
||||
return request
|
||||
}
|
||||
|
||||
private func requestWithInjectedRules(_ request: LyricsRevisionRequest) throws -> LyricsRevisionRequest {
|
||||
var request = request
|
||||
request.context.injectPrivateApplicationRules(try ruleProvider.privateApplicationRules())
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import Foundation
|
||||
|
||||
public final class SongProjectDiscussionDirector: Sendable {
|
||||
private let aiService: any AIService
|
||||
|
||||
public init(aiService: any AIService) {
|
||||
self.aiService = aiService
|
||||
}
|
||||
|
||||
public func discuss(
|
||||
project: SongProject,
|
||||
userMessage: String,
|
||||
conversation: [AIConversationMessage] = [],
|
||||
localeIdentifier: String? = nil
|
||||
) async throws -> SongProjectDiscussionResult {
|
||||
guard project.conversationMode == .discuss else {
|
||||
throw SongProjectDiscussionDirectorError.discussionModeNotEnabled
|
||||
}
|
||||
|
||||
let trimmedMessage = userMessage.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmedMessage.isEmpty else {
|
||||
throw SongProjectDiscussionDirectorError.emptyUserMessage
|
||||
}
|
||||
|
||||
let result = try await aiService.discussSongProject(
|
||||
from: SongProjectDiscussionRequest(
|
||||
context: AIRequestContext(
|
||||
userInstruction: trimmedMessage,
|
||||
conversation: conversation,
|
||||
localeIdentifier: localeIdentifier
|
||||
),
|
||||
project: project
|
||||
)
|
||||
)
|
||||
|
||||
return SongProjectDiscussionResult(
|
||||
questions: result.questions
|
||||
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
.filter { !$0.isEmpty },
|
||||
notes: result.notes
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public enum SongProjectDiscussionDirectorError: Error, Equatable, Sendable {
|
||||
case discussionModeNotEnabled
|
||||
case emptyUserMessage
|
||||
}
|
||||
Reference in New Issue
Block a user