From 38996b71b2cfffff87b0427ada3594f958f4f22a Mon Sep 17 00:00:00 2001 From: Dante Ausonio Date: Wed, 3 Jan 2024 20:15:15 -0800 Subject: [PATCH 1/3] Create Contributions --- Sources/OpenAI/Contributions | Bin 0 -> 1080 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 Sources/OpenAI/Contributions diff --git a/Sources/OpenAI/Contributions b/Sources/OpenAI/Contributions new file mode 100644 index 0000000000000000000000000000000000000000..bbe1aa2597554854b985dc0f7036f6e7335fa831 GIT binary patch literal 1080 zcmZvb&ubG=5XWcJ)Ye}#ZAFVBF5*FikR`wC$?Yamp~W29BI0F}Z0(YCH|%b}V}&04 z1F8qTSnWZu;H5W3D%iUR4<0;v=uHm_o{Znw%xj1^eiJ{AIhgyS1Htd0_TEqHl?Q%E$;J$Q?zaX++DR6NKPX;x}zCa_nyCd#*nwv5FJ6 zBELfu;#=JyaJ)#j+IAQ^A)S=?!op%Lup5!vaOk|mZg(8dyj8OsE&IM>`Cb&b^={<) z9u_?|ltXk{%|Dz*S2&JjKAaBap$c>l>WH?`?m**E5@+1xPMGqa?D4?|{;P>z98mqQ z?_R{M4Em$DD||&q<3BuIAM!u`<0sw>_n#~Hf~ZiD)D!CZxEwv;W+7ShJexk5T$E#=a)b7nPd zSY|nI6mynQEUL3S#5@>6-=~LLj`4fUpf#9r)~4syfpI>!|FfXkURC#V2IK$qdOe(|xhy&gP6?;Ly6_};Nq7pJ z7oG-Jgmv(SumM){Yx_h$Wvxjtx?-E!$9W`Q`!vW;fw{J9=YI762h5kjyORGGRAhTU literal 0 HcmV?d00001 From 70420c814f713029cb3362136bde15c064c2fa46 Mon Sep 17 00:00:00 2001 From: Dante Ausonio Date: Wed, 3 Jan 2024 20:44:06 -0800 Subject: [PATCH 2/3] Update Contributions --- Sources/OpenAI/Contributions | Bin 1080 -> 1080 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/Sources/OpenAI/Contributions b/Sources/OpenAI/Contributions index bbe1aa2597554854b985dc0f7036f6e7335fa831..82da05181762e7021f514b3f79ab796f4a351d1c 100644 GIT binary patch delta 16 XcmdnNv4dlR25YY)bF=wIU2YZtEye`4 delta 16 YcmdnNv4dlR2CLSZDGQA@>T Date: Wed, 3 Jan 2024 21:18:57 -0800 Subject: [PATCH 3/3] Trying to integrate changes I'm trying to integrate my changes. Just copied all of my files over but now it's bitching at me because it needs to use API key, but how am I supposed to pass API key into a swift package? --- Sources/OpenAI/Contributions | Bin 1080 -> 0 bytes .../Agents/InjectorAgent.swift | 135 ++++++++++++++++++ .../Extensions/StringExtensions.swift | 17 +++ .../Model/AIMemoryModels.swift | 126 ++++++++++++++++ .../Model/ChatSystems.swift | 46 ++++++ .../Model/ConversationModel.swift | 97 +++++++++++++ 6 files changed, 421 insertions(+) delete mode 100644 Sources/OpenAI/Contributions create mode 100644 Sources/OpenAI/Dante_Contributions/Agents/InjectorAgent.swift create mode 100644 Sources/OpenAI/Dante_Contributions/Extensions/StringExtensions.swift create mode 100644 Sources/OpenAI/Dante_Contributions/Model/AIMemoryModels.swift create mode 100644 Sources/OpenAI/Dante_Contributions/Model/ChatSystems.swift create mode 100644 Sources/OpenAI/Dante_Contributions/Model/ConversationModel.swift diff --git a/Sources/OpenAI/Contributions b/Sources/OpenAI/Contributions deleted file mode 100644 index 82da05181762e7021f514b3f79ab796f4a351d1c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1080 zcmZvbJ#5oZ5Xa9+$~S7-BB($J3o)=nv6{~^;W|M@YDI>&LWsplTxun?BioS}i|D|@ z1`M$9(E$l0mTn+aB)T&&FfcN7g8?Ka!vCCmjf^H8gmt=mD|R>3UF z#GfpG+3uU^yOkHa=FlVhmKvs{(}*B<42`A{Er(7Jf{%&cw!O%)`=Rf-{y~Y=oUjx5 zJsKC^>IZ@2MY`3s!_W!osKgf*78`-xirkh%rzLi+=XmDThTZDew;ap&qQGtTBiHw^ z=z&BI(Q!3@GL0^99Lan*9V$XK=qA(?JwUq!9fC4A<0f~)l>ca(4?gl=jt}C1>VI|P zEN*4gAH7}SE7~9b;qiLH|M-uedXwxwQuX3|wf8;D^_%Czolip-p(->FU5C~n-V4{p z+W7II?El*gCX>ND%o<3-(w!F!Hl!EJ+}#r^SS+>1?}#tx}Osm|7S25@LX&s zp$s0)yV%41fNx7@7Q8Kd8N4OD1pX?#4Biu70e==w=|oRu(J^pVI1AQ=C%{X>li;H8 y6u2g=gExc?u$o`nA^It6O@PrA+vE<;Bl+5=QGOQ8wPic?WB5N{J`a8^`F{a|>Uxa; diff --git a/Sources/OpenAI/Dante_Contributions/Agents/InjectorAgent.swift b/Sources/OpenAI/Dante_Contributions/Agents/InjectorAgent.swift new file mode 100644 index 00000000..56344b62 --- /dev/null +++ b/Sources/OpenAI/Dante_Contributions/Agents/InjectorAgent.swift @@ -0,0 +1,135 @@ +// +// InjectorAgent.swift +// OpenAIPackageContributions +// +// Created by Dante Ausonio on 12/14/23. +// + +import Foundation + +class InjectorAgent { + + private let openAI = OpenAI(apiToken: APIKeys().OpenAIKey) + let system: ChatSystem + let memoryStrategy: MemoryStrategyInterface + var scope: MemoryStrategyInterface = CMS_Scope() + let checkInRate: Int + var currentPhase = 0 + + let knowledgeBase = "" + let tools = "" + + + + + init(process: ChatSystem, checkInRate: Int = 3, memoryStrategy: MemoryStrategyInterface = CMS_UserContext()) { + self.system = process + self.checkInRate = checkInRate + self.memoryStrategy = memoryStrategy + } + + + + + func initChat() -> String { + return "\(system.baseBehaviorPrompt)\n\(system.phases[0].prompt)" + } + + + func performCheckIn(on chat: Conversation) async throws -> String { + print("\n\n----------------------- Perfomring Check In -----------------------\n") + do { + async let scopeString = try scope.setAndGetMemoryString(for: chat) + async let memoryString = try memoryStrategy.setAndGetMemoryString(for: chat) + async let phaseNumber = try self.getPhaseNumber(chat) + + try self.currentPhase = await phaseNumber + let systemPromptInjection = try await self.formatInjectionPrompt(phaseNumber, await scopeString, await memoryString) + + return systemPromptInjection + } catch { + print("perform check in failed ", error) + throw error + } + } + + + private func getPhaseNumber(_ chat: Conversation) async throws -> Int { + do { + let agentMessage = chat.messages + [Chat(role: .system, content: self.phaseProgressionPrompt(for: chat))] + let query = ChatQuery(model: .gpt3_5Turbo_1106, messages: agentMessage, n: 1) + let result = try await openAI.chats(query: query) + guard let response = result.choices.first?.message.content else { print("JSON FAILED"); return 0 } + if let phaseNumber = Int(response) { + return phaseNumber + } else { + return 0 + } + } catch { + throw error + } + } + + + + private func formatInjectionPrompt(_ phaseNumber: Int, _ scopeStr: String, _ memoryStr: String) -> String { + let phasePrompt = system.phases[phaseNumber].prompt + let systemPrompt = "\(system.baseBehaviorPrompt) You rely on the 3 following pieces of information to guide your interactions. Conversation Scope, Memory, and Instructions.\n\nConversation Scope:\n\(scopeStr)\n\nMemory:\n\(self.memoryStrategy.memoryDescription)\n\(memoryStr)\n\nInstructions:\n\(phasePrompt)" + print("\n\n---------------------------Formatted System Prompt:----------------------------\n\(systemPrompt)") + return systemPrompt + } + + +} + + + + + + +// MARK: Helper Functions +extension InjectorAgent { + func shouldCheckIn(on chat: Conversation) -> Bool { + let chatCount = chat.chatHistory().count + return (chatCount % self.checkInRate == 0 || (chatCount - 1) % self.checkInRate == 0) && (chatCount > 1) + } +} + + + + + + + + +// MARK: Prompts +extension InjectorAgent { + + + private func phaseProgressionPrompt(for chat: Conversation) -> String { + var phaseDescriptions = "" + var i = 0 + for phase in system.phases { + phaseDescriptions.append("\(phase.name):\nIndex Number: \(i)\nPhase Description:\(phase.description)\n--------------------------------\n") + i += 1 + } + + if let prgoressionInstructions = system.phaseProgressionInstruction { + return prgoressionInstructions + } + + switch system.progressionStyle { + case .linear: + return "You are a conversational manager. You manage the conversation between a Conversational AI Assistant and a User. There is a set of possible conversational phases the AI can enter. Each phase defines the behavior of the AI Assistant. The phases must progress in order from one to the next. It is your job to determine when to progress to the next phase. The AI is currently in phase number \(self.currentPhase). Decide if the conversation should progress to the next phase based on the scope of the conversation, the conversation so far, and the phase descriptions. Return an integer for the index number associated with the appropriate conversational phase the AI should enter. Return only the index number, and nothing else. In other words, your response should be a number between 0 and \(system.phases.count), and that number should be the index number of the phase the AI Assistant should enter. \n\n\nConversation Scope:\n \(self.scope.memoryString) \n\n\n Phase Descriptions:\(phaseDescriptions)" + case .nonlinear: + return "You are a conversational manager. You manage the conversation between a Conversational AI Assistant and a User. There is a set of possible conversational phases the AI can enter. Each phase defines the behavior of the AI Assistant. The phases can be entered in any order and do NOT have to come one after the other. It is your job to determine which phase is most useful at this point in the conversation. The AI is currently in phase number \(self.currentPhase). Decide phase the assistant should enter based on the scope of the conversation, the conversation so far, and the phase descriptions. Return an integer for the index number associated with the appropriate conversational phase the AI should enter. Return only the index number, and nothing else. In other words, your response should be a number between 0 and \(system.phases.count), and that number should be the index number of the phase the AI Assistant should enter. \n\n\nConversation Scope:\n \(self.scope.memoryString) \n\n\n Phase Descriptions:\(phaseDescriptions)" + } + + } +} + + + + + +// TODO: Figure out how to create a memory system using the Strategy Design Pattern. diff --git a/Sources/OpenAI/Dante_Contributions/Extensions/StringExtensions.swift b/Sources/OpenAI/Dante_Contributions/Extensions/StringExtensions.swift new file mode 100644 index 00000000..f201b807 --- /dev/null +++ b/Sources/OpenAI/Dante_Contributions/Extensions/StringExtensions.swift @@ -0,0 +1,17 @@ +import Foundation +import NaturalLanguage + +extension String { + var tokenCount: Int { + let tokenizer = NLTokenizer(unit: .word) + tokenizer.string = self + var tokenCount = 0 + tokenizer.enumerateTokens(in: self.startIndex.. String + func setMemoryString(for chat: Conversation) async throws + func queryMemory(query: String) -> String + +} + + +// CMS = Concrete Memory Strategy + +//MARK: Scope Memory +class CMS_Scope: MemoryStrategyInterface { + + private let openAI = OpenAI(apiToken: APIKeys().OpenAIKey) + private var updateScopePrompt: String = "" + var memoryString: String = "" + var memoryDescription: String + + private var defaultUpdateScopePrompt: String { + return "You are a conversational manager. You manage the conversation between a User and a conversational AI Assistant. Your task is to update the Scope Description for the conversation in order give the AI conversational memory. This memory is used to determine which conversational paths should be chosen. The Scope Description tells the AI Assistant what the topic of conversation is, what the conversation is about, what has been discussed, what should be discussed and serves as an overall summary for the conversation thus far. The cope should be abstract enough to capture the essence of the conversation with out getting bogged down in the details. It is a high level overview of the conversation, meant for keeping the AI on track. If there is no new relevant information, simply return the original context description. Don't generate repetative information. Keep everything concise and to the point. \nHere is the current Scope Description: \(self.memoryString)\nReturn json containing the updated Scope Description in the form {'description': string value}." + } + private var defaultMemoryDescription: String = "Your memory describes the scope of the conversation" + + init(customUpdateScopePrompt: String? = nil, customMemoryDescription: String? = nil) { + self.memoryDescription = customMemoryDescription ?? defaultMemoryDescription + self.updateScopePrompt = customUpdateScopePrompt ?? defaultUpdateScopePrompt + + } + + func setAndGetMemoryString(for chat: Conversation) async throws -> String { + do { + try await setMemoryString(for: chat) + return self.memoryString + } catch { + print("CMS_Scope.getMemoryString Failed! \(error)") + throw error + } + } + + func setMemoryString(for chat: Conversation) async throws { + do { + let agentMessage = chat.messages + [Chat(role: .system, content: self.updateScopePrompt)] + let query = ChatQuery(model: .gpt3_5Turbo_1106, messages: agentMessage, responseFormat: .jsonObject, n: 1) + + let result = try await openAI.chats(query: query) + guard let json = result.choices.first?.message.content?.data(using: .utf8) else { return } + let decodedJSON = try JSONDecoder().decode([String: String].self, from: json) + self.memoryString = decodedJSON["description"] ?? "" + } catch { + print("CMS_Scope.getMemoryString Failed! \(error)") + throw error + } + } + + func queryMemory(query: String = "") -> String { + return memoryString + } +} + + + +// MARK: User Context Memory +class CMS_UserContext: MemoryStrategyInterface { + private let openAI = OpenAI(apiToken: APIKeys().OpenAIKey) + private var updateMemoryPrompt: String = "" + var memoryDescription: String + var memoryString: String = "" + + var defaultMemoryDescription: String = "Your memory provides a list of facts known about the user." + var defautUpdateMemoryPrompt: String {"You are a conversational manager. You manage a conversation held between a conversational AI assistnat and a user. Your task is to update the user context memory of the conversational AI using the conversation so far. The User Context Memory is a bullet point list of facts learned about the user during the conversation that contain key details and information about the user. It should contain specific details not general statements. remembered If there is no new relevant information, simply return the original context description. Don't generate repetative information. Keep everything concise and to the point. \nHere is the current user context memory: \(self.memoryString)\n Return JSON containing the the updated User Context Memory in the form {'description': context} where context is the fact list formatted as a string." + } + + init(customUpdateScopePrompt: String? = nil, customMemoryDescription: String? = nil) { + self.memoryDescription = customMemoryDescription ?? defaultMemoryDescription + self.updateMemoryPrompt = customUpdateScopePrompt ?? defautUpdateMemoryPrompt + + } + + func setAndGetMemoryString(for chat: Conversation) async throws -> String { + try await setMemoryString(for: chat) + return self.memoryString + } + + func setMemoryString(for chat: Conversation) async throws { + do { + let agentMessage = chat.messages + [Chat(role: .system, content: self.updateMemoryPrompt)] + let query = ChatQuery(model: .gpt3_5Turbo_1106, messages: agentMessage, responseFormat: .jsonObject, n: 1) + + let result = try await openAI.chats(query: query) + guard let json = result.choices.first?.message.content?.data(using: .utf8) else { return } + let decodedJSON = try JSONDecoder().decode([String: String].self, from: json) + self.memoryString = decodedJSON["description"] ?? "" + print("\nCMS_UserContext memory Decoded and Formatted Result:\n", decodedJSON) + } catch { + print("CMS_UserContext.getMemoryString() Failed! \(error)") + throw error + } + } + + func queryMemory(query: String = "") -> String { + return memoryString + } +} + + + +// MARK: More Memory Strategies... + + + + + + + diff --git a/Sources/OpenAI/Dante_Contributions/Model/ChatSystems.swift b/Sources/OpenAI/Dante_Contributions/Model/ChatSystems.swift new file mode 100644 index 00000000..0846f0af --- /dev/null +++ b/Sources/OpenAI/Dante_Contributions/Model/ChatSystems.swift @@ -0,0 +1,46 @@ +// +// ProcessDescriptions.swift +// OpenAIPackageContributions +// +// Created by Dante Ausonio on 12/24/23. +// + +import Foundation + +struct ChatSystem { + let description: String /// Answers what the system does or is used for. It allows the observer to know why the process exists and why it might be chosen. + + let phases: [Phase] /// Each phase adjusts the behavior of the LLM allowing for an evolving conversation. + + let progressionStyle: phaseProgression /// How should the Observer progress through each phase? Linear = Progress through each phase in order, Nonlinear = jump to which ever phase seems fit. + + let baseBehaviorPrompt: String /// This sets the assistants behavior. It is used in all system prompts. This should set the role, identity and overall personality of the LLM. Each phase then adjusts the way this entity acts and provides more specific instructions. + + let phaseProgressionInstruction: String? = nil /// Instructs the Observer AI on how the phases should progress. + + + /// Not yet fully implemented + enum phaseProgression { + case linear, nonlinear + + var prompt: String { + switch self { + case .linear: /// Each phase comes one after the other in order of placement in the phases array + return "" + case .nonlinear: /// No order to phases. Any phase can come at any time + return "" + } + } + + } +} + + +/// Each phase defines a particular kind of behavior that the conversational AI can take on in a chat system +struct Phase { + var name: String /// Name to identify or reference the phase + + var description: String /// Describes the phase to the Observer AI so it knows when and why to chose it + + var prompt: String /// The system prompt to injected into the conversational ai that defines its behavior for the conversational phase +} diff --git a/Sources/OpenAI/Dante_Contributions/Model/ConversationModel.swift b/Sources/OpenAI/Dante_Contributions/Model/ConversationModel.swift new file mode 100644 index 00000000..62146d4c --- /dev/null +++ b/Sources/OpenAI/Dante_Contributions/Model/ConversationModel.swift @@ -0,0 +1,97 @@ +// +// MainChatModel.swift +// Demo +// +// Created by Dante Ausonio on 12/11/23. +// + +import Foundation + +class Conversation: Identifiable { + var title: String = "" + var id = UUID() + var date = Date() + var messages: [Chat] = [] // For AI + var UIMessages : [Message] { //For UI + return messages.map { data in + return Message(role: data.role, content: data.content ?? "") + } + } + + init(title: String) { + self.title = title + } + + init(id: UUID, date: Date, conversation: [Chat], title: String) { + self.id = id + self.date = date + self.messages = conversation + self.title = title + } + +} + + +// MARK: Conversation Functions +extension Conversation { + func appendSystemPrompt(_ prompt: String) { + messages.append(Chat(role: .system, content: prompt)) + } + + func appendUserMessage(_ message: String) { + messages.append(Chat(role: .user, content: message)) + } + + func appendAssistantResponse(_ response: String) { + messages.append(Chat(role: .assistant, content: response)) + } + + +} + + + + +// MARK: Conversation History +extension Conversation { + + /// Returns all chats from the user or the assistant + func chatHistory() -> [Chat] { + return messages.filter { message in + return message.role == Chat.Role.assistant || message.role == Chat.Role.user + } + } + + /// Returns last n chats with in a certain token limit, and the last system prompt. + func bufferHistory(tokenLimit: Int) -> [Chat] { + var bufferHistory = [Chat]() + var tokenCount = 0 + + for message in chatHistory().reversed() { + tokenCount += message.content?.tokenCount ?? 0 + if tokenCount >= tokenLimit { + break + } + bufferHistory.insert(message, at: 0) + } + + if let systemPrompt = messages.last(where: { $0.role == .system }) { + bufferHistory.insert(systemPrompt, at: 0) + } + + return bufferHistory + } +} + + +/// Object that contains a message in such a way that it can be used for display in UI +struct Message: Identifiable { + let id = UUID() + let date = Date() + let role: Chat.Role + let content: String +} + + + +