Skip to content

Commit

Permalink
Add OpenAI Interactions with a Ask Questions Section and Summary Prom…
Browse files Browse the repository at this point in the history
…pt (#15)
  • Loading branch information
PSchmiedmayer authored Aug 30, 2023
1 parent 606142a commit 6ab3df7
Show file tree
Hide file tree
Showing 21 changed files with 1,015 additions and 273 deletions.
142 changes: 118 additions & 24 deletions OwnYourData.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@
"version" : "1.2022062300.0"
}
},
{
"identity" : "fhirmodels",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/FHIRModels",
"state" : {
"revision" : "861afd5816a98d38f86220eab2f812d76cad84a0",
"version" : "0.5.0"
}
},
{
"identity" : "firebase-ios-sdk",
"kind" : "remoteSourceControl",
Expand Down Expand Up @@ -63,6 +72,15 @@
"version" : "3.1.1"
}
},
{
"identity" : "healthkitonfhir",
"kind" : "remoteSourceControl",
"location" : "https://github.com/StanfordBDHG/HealthKitOnFHIR.git",
"state" : {
"revision" : "fdf8e4543718a940643598e4bd5e750e9c4c5540",
"version" : "0.2.4"
}
},
{
"identity" : "imagesource",
"kind" : "remoteSourceControl",
Expand Down Expand Up @@ -148,17 +166,17 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/StanfordSpezi/SpeziHealthKit.git",
"state" : {
"branch" : "newSpeziStandard",
"revision" : "9ec56e35ccb47b322402bdd123cdbb18c3900ff8"
"revision" : "f8f664549e81c8fa107a1fff616e0eaca6e8a6fa",
"version" : "0.3.1"
}
},
{
"identity" : "speziml",
"kind" : "remoteSourceControl",
"location" : "https://github.com/StanfordSpezi/SpeziML.git",
"state" : {
"revision" : "700ab5e524ec4f12f3012b514ea24822bdc6066b",
"version" : "0.2.2"
"branch" : "feature/apiImprovements",
"revision" : "8d3be96589f9ac30342b537917d37ff00d4ee058"
}
},
{
Expand Down
96 changes: 96 additions & 0 deletions TemplateApplication/ChatView/FHIRMultipleResourceInterpreter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
//
// This source file is part of the Stanford OwnYourData Application project
// File originates from the Stanford LLMonFHIR project
//
// SPDX-FileCopyrightText: 2023 Stanford University
//
// SPDX-License-Identifier: MIT
//

import OpenAI
import Spezi
import SpeziLocalStorage
import SpeziOpenAI
import SwiftUI


private enum FHIRMultipleResourceInterpreterConstants {
static let storageKey = "FHIRMultipleResourceInterpreter.Cache"
}


class FHIRMultipleResourceInterpreter: DefaultInitializable, Component, ObservableObject, ObservableObjectProvider {
@Dependency private var localStorage: LocalStorage
@Dependency private var openAIComponent: OpenAIComponent


var interpretation: String? {
willSet {
Task { @MainActor in
objectWillChange.send()
}
}
didSet {
do {
try localStorage.store(interpretation, storageKey: FHIRMultipleResourceInterpreterConstants.storageKey)
} catch {
print(error)
}
}
}


required init() {}


func configure() {
guard let cachedInterpretation: String = try? localStorage.read(
storageKey: FHIRMultipleResourceInterpreterConstants.storageKey
) else {
return
}
self.interpretation = cachedInterpretation
}


func askQuestionsChat(forResources resources: [FHIRResource]) -> [Chat] {
var resourceCategories = String()

for resource in resources {
resourceCategories += (resource.functionCallIdentifier + "\n")
}

return [
Chat(
role: .system,
content: Prompt.allResources.prompt.replacingOccurrences(of: Prompt.promptPlaceholder, with: resourceCategories)
)
]
}


func generateSummary(forResources resources: [FHIRResource]) async throws -> String {
var resourceCategories = String()

for resource in resources {
resourceCategories += (resource.compactJSONDescription + "\n\n")
}

let chat = [
Chat(
role: .system,
content: Prompt.summary.prompt.replacingOccurrences(of: Prompt.promptPlaceholder, with: resourceCategories)
)
]

let chatStreamResults = try await openAIComponent.queryAPI(withChat: chat)
var summary = ""
for try await chatStreamResult in chatStreamResults {
for choice in chatStreamResult.choices {
summary += choice.delta.content ?? ""
}
}

return summary
}
}
200 changes: 200 additions & 0 deletions TemplateApplication/ChatView/OpenAIChatView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
//
// This source file is part of the Stanford OwnYourData Application project
// File originates from the Stanford LLMonFHIR project
//
// SPDX-FileCopyrightText: 2023 Stanford University
//
// SPDX-License-Identifier: MIT
//

import OpenAI
import SpeziOpenAI
import SpeziViews
import SwiftUI


struct OpenAIChatView: View {
@Environment(\.dismiss) private var dismiss
@EnvironmentObject private var openAPIComponent: OpenAIComponent
@EnvironmentObject private var fhirStandard: FHIR

@State private var chat: [Chat]
@State private var viewState: ViewState = .idle
@State private var systemFuncMessageAdded = false

private let enableFunctionCalling: Bool
private let title: String


private var disableInput: Binding<Bool> {
Binding(
get: {
viewState == .processing
},
set: { _ in }
)
}

var body: some View {
NavigationStack {
ChatView($chat, disableInput: disableInput)
.navigationTitle(title)
.viewStateAlert(state: $viewState)
.onChange(of: chat) { _ in
getAnswer()
}
.onAppear {
getAnswer()
}
}
}


init(chat: [Chat], title: String, enableFunctionCalling: Bool) {
self._chat = State(initialValue: chat)
self.title = title
self.enableFunctionCalling = enableFunctionCalling
}


private func getAnswer() {
guard viewState == .idle, chat.last?.role == .user || (chat.count == 1 && chat.first?.role == .system) else {
return
}

Task {
do {
viewState = .processing

if enableFunctionCalling {
if systemFuncMessageAdded == false {
try await addSystemFuncMessage()
systemFuncMessageAdded = true
}
try await processFunctionCalling()
}

try await processChatStreamResults()

viewState = .idle
} catch let error as APIErrorResponse {
viewState = .error(error)
} catch {
viewState = .error(error.localizedDescription)
}
}
}

private func addSystemFuncMessage() async throws {
let resourcesArray = fhirStandard.resources

var stringResourcesArray = resourcesArray.map { $0.functionCallIdentifier }
stringResourcesArray.append("N/A")

self.chat.append(Chat(role: .system, content: String(localized: "FUNCTION_CONTEXT") + stringResourcesArray.rawValue))
}

private func processFunctionCalling() async throws {
let resourcesArray = fhirStandard.resources

var stringResourcesArray = resourcesArray.map { $0.functionCallIdentifier }
stringResourcesArray.append("N/A")

let functionCallOutputArray = try await getFunctionCallOutputArray(stringResourcesArray)

processFunctionCallOutputArray(functionCallOutputArray: functionCallOutputArray, resourcesArray: resourcesArray)
}

private func getFunctionCallOutputArray(_ stringResourcesArray: [String]) async throws -> [String] {
let functions = [
ChatFunctionDeclaration(
name: "get_resource_titles",
description: String(localized: "FUNCTION_DESCRIPTION"),
parameters: JSONSchema(
type: .object,
properties: [
"resources": .init(type: .string, description: String(localized: "PARAMETER_DESCRIPTION"), enumValues: stringResourcesArray)
],
required: ["resources"]
)
)
]

let chatStreamResults = try await openAPIComponent.queryAPI(withChat: chat, withFunction: functions)


class ChatFunctionCall {
var name: String = ""
var arguments: String = ""
var finishReason: String = ""
}

let functionCall = ChatFunctionCall()

for try await chatStreamResult in chatStreamResults {
for choice in chatStreamResult.choices {
if let deltaName = choice.delta.name {
functionCall.name += deltaName
}
if let deltaArguments = choice.delta.functionCall?.arguments {
functionCall.arguments += deltaArguments
}
if let finishReason = choice.finishReason {
functionCall.finishReason += finishReason
if finishReason == "get_resource_titles" { break }
}
}
}

guard functionCall.finishReason == "function_call" else {
return []
}

let trimmedArguments = functionCall.arguments.trimmingCharacters(in: .whitespacesAndNewlines)

guard let resourcesRange = trimmedArguments.range(of: "\"resources\": \"([^\"]+)\"", options: .regularExpression) else {
return []
}

return trimmedArguments[resourcesRange]
.replacingOccurrences(of: "\"resources\": \"", with: "")
.replacingOccurrences(of: "\"", with: "")
.components(separatedBy: ",")
}

private func processFunctionCallOutputArray(functionCallOutputArray: [String], resourcesArray: [FHIRResource]) {
for resource in functionCallOutputArray {
guard let matchingResource = resourcesArray.first(where: { $0.functionCallIdentifier == resource }) else {
continue
}

let functionContent = """
Based on the function get_resource_titles you have requested the following health records: \(resource).
This is the associated JSON data for the resources which you will use to answer the users question:
\(matchingResource.jsonDescription)
Use this health record to answer the users question ONLY IF the health record is applicable to the question.
"""

chat.append(Chat(role: .function, content: functionContent, name: "get_resource_titles"))
}
}

private func processChatStreamResults() async throws {
let chatStreamResults = try await openAPIComponent.queryAPI(withChat: chat)

for try await chatStreamResult in chatStreamResults {
for choice in chatStreamResult.choices {
if chat.last?.role == .assistant {
let previousChatMessage = chat.last ?? Chat(role: .assistant, content: "")
chat[chat.count - 1] = Chat(
role: .assistant,
content: (previousChatMessage.content ?? "") + (choice.delta.content ?? "")
)
} else {
chat.append(Chat(role: .assistant, content: choice.delta.content ?? ""))
}
}
}
}
}
Loading

0 comments on commit 6ab3df7

Please sign in to comment.