Implement optional discussion mode

This commit is contained in:
diyaa
2026-09-13 21:13:52 +02:00
parent bf1ccbe8a9
commit a39bdbe6fc
12 changed files with 291 additions and 1 deletions
@@ -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
}