diff --git a/Package.swift b/Package.swift index ef2cd5c..1644250 100644 --- a/Package.swift +++ b/Package.swift @@ -20,7 +20,10 @@ let package = Package( dependencies:["BotsKit"]), Target( name:"SwiftBot", - dependencies:["ChatProviders","Storage","EchoBot"]) + dependencies:["ChatProviders","Storage","EchoBot"]), + Target( + name:"CoffeeBot", + dependencies:["BotsKit"]) ], dependencies: [ .Package(url: "https://github.com/PerfectlySoft/Perfect-HTTPServer.git", majorVersion: 2), diff --git a/Sources/CoffeeBot/CoffeeBot.swift b/Sources/CoffeeBot/CoffeeBot.swift new file mode 100644 index 0000000..6061985 --- /dev/null +++ b/Sources/CoffeeBot/CoffeeBot.swift @@ -0,0 +1,48 @@ +// +// CoffeeBot.swift +// CoffeeBot +// + +import LoggerAPI +import BotsKit + +internal protocol CoffeeBotInputProtocol { + func send(activity:Activity) +} + +public final class CoffeeBot : Bot { + + //MARK: Bot Protocol Properties + public let name = "CoffeeBot" + public var sendActivity: Signal = Signal() + + //MARK: Other Properties + private let coffeeManager : CoffeeManager + + //MARK: Bot Protocol Methods + public func dispatch(activity: Activity) -> DispatchResult { + self.coffeeManager.process(activity: activity) + return .ok + } + + //MARK: Lifecycle + public init() { + let sessionService = SessionService() + let appointingService = AppointingService() + let spellCheckerService = SpellCheckingService() + let entityRecognitionService = EntityRecognitionService() + let textProcessingService = TextProcessingService(spellCheckerService: spellCheckerService, + entityRecognitionService: entityRecognitionService) + + self.coffeeManager = CoffeeManager(textProcessingService: textProcessingService, + appointingService:appointingService, + sessionService:sessionService) + self.coffeeManager.botDelegate = self + } +} + +extension CoffeeBot : CoffeeBotInputProtocol { + func send(activity: Activity) { + self.sendActivity.update(activity) + } +} diff --git a/Sources/CoffeeBot/CoffeeManager/CoffeeManager.swift b/Sources/CoffeeBot/CoffeeManager/CoffeeManager.swift new file mode 100644 index 0000000..a344465 --- /dev/null +++ b/Sources/CoffeeBot/CoffeeManager/CoffeeManager.swift @@ -0,0 +1,94 @@ +// +// CoffeeManager.swift +// SwiftBot +// + +import Foundation +import BotsKit +import LoggerAPI + +internal protocol CoffeeManagerProtocol { + func process(activity:Activity) +} + +class CoffeeManager { + + //MARK: Properties + fileprivate var session : Session? = nil + fileprivate let textProcessingService : TextProcessingService + fileprivate let appointingService : AppointingService + fileprivate let sessionService : SessionService + weak var botDelegate : CoffeeBot? + + //MARK: Lifecycle + init(textProcessingService:TextProcessingService, appointingService:AppointingService, sessionService:SessionService) { + self.textProcessingService = textProcessingService + self.appointingService = appointingService + self.sessionService = sessionService + } + + //MARK: Private + fileprivate func hasEveryoneReplied(in conversation:Conversation, with currentSession:Session) -> Bool { + return currentSession.membersReplied.count == conversation.members.count + } + + fileprivate func isTimeoutPeriodFinished() -> Bool { + //TODO: Set a timeout for messages + //E.g. all coffee requests should be made within 2 minutes. + return false + } + + fileprivate func acknowledgeInputs(with activity:Activity, for session:Session) { + self.post(update: session.description, on: activity) + } + + fileprivate func postProcessingResult(chosenPerson:Account, session:Session, activity:Activity) { + let update = "\(chosenPerson.name) was selected to retrieve \(session.description). Congrats! ☕️" + self.post(update: update, on: activity) + } + + fileprivate func post(update text:String, on activity:Activity) { + let updatedActivity = activity.replay(text: text) + self.botDelegate?.send(activity: updatedActivity) + } +} + +extension CoffeeManager : CoffeeManagerProtocol { + + func process(activity: Activity) { + + //Restoring current context + self.session = self.sessionService.restoreSession() + guard var session = self.session else { return } + + //Processing the current message + let processingResult = self.textProcessingService.process(activity.text) + + //Saving the coffee to session + switch processingResult { + case .success(let coffees): + let coffeesRequested = CoffeeRequest(person: activity.from, coffees: coffees) + session.coffeesRequested.append(coffeesRequested) + self.sessionService.save(session: session) + + case .error(let error): + Log.error("An error occured during processing message:\(error.localizedDescription)") + return + } + + //Dispatching further events + if + hasEveryoneReplied(in: activity.conversation, with: session) || + isTimeoutPeriodFinished() + { + //Printing acknowledgement of current requests + self.acknowledgeInputs(with: activity, for: session) + + self.post(update: "Finding the lucky one...", on: activity) + + //proceed to next steps + let luckyPerson = self.appointingService.appointJobToAPerson(with: session) + self.postProcessingResult(chosenPerson: luckyPerson, session: session, activity: activity) + } + } +} diff --git a/Sources/CoffeeBot/Models/Coffee.swift b/Sources/CoffeeBot/Models/Coffee.swift new file mode 100644 index 0000000..f92911c --- /dev/null +++ b/Sources/CoffeeBot/Models/Coffee.swift @@ -0,0 +1,18 @@ +// +// Coffee.swift +// Coffee +// + +internal struct Coffee { + let amount : Int + let type : CoffeeType + let additions : [CoffeeAddition] + var description : String { + get { + let additionsDescription = self.additions.map { (addition) -> String in + return addition.rawValue + } + return "\(self.type.rawValue) with \(additionsDescription.joined(separator:" "))" + } + } +} diff --git a/Sources/CoffeeBot/Models/CoffeeAddition.swift b/Sources/CoffeeBot/Models/CoffeeAddition.swift new file mode 100644 index 0000000..f337be4 --- /dev/null +++ b/Sources/CoffeeBot/Models/CoffeeAddition.swift @@ -0,0 +1,9 @@ +// +// CoffeeAddition.swift +// CoffeeAddition +// + +public enum CoffeeAddition : String { + case milk + case sugar +} diff --git a/Sources/CoffeeBot/Models/CoffeeRequest.swift b/Sources/CoffeeBot/Models/CoffeeRequest.swift new file mode 100644 index 0000000..923bf31 --- /dev/null +++ b/Sources/CoffeeBot/Models/CoffeeRequest.swift @@ -0,0 +1,19 @@ +// +// CoffeeRequest.swift +// CoffeeRequest +// + +import Foundation +import BotsKit + +internal struct CoffeeRequest { + let person : Account + let coffees : [Coffee] + + var description : String { + let coffeeDescriptions = coffees.map { (coffee) -> String in + return coffee.description + } + return "\(person.name) would like to have: \n \(coffeeDescriptions.joined(separator:" "))" + } +} diff --git a/Sources/CoffeeBot/Models/CoffeeType.swift b/Sources/CoffeeBot/Models/CoffeeType.swift new file mode 100644 index 0000000..248945f --- /dev/null +++ b/Sources/CoffeeBot/Models/CoffeeType.swift @@ -0,0 +1,10 @@ +// +// CoffeeType.swift +// CoffeeType +// + +internal enum CoffeeType : String { + case latte + case capuccino + case filterCoffee +} diff --git a/Sources/CoffeeBot/Models/Session.swift b/Sources/CoffeeBot/Models/Session.swift new file mode 100644 index 0000000..07f2c1c --- /dev/null +++ b/Sources/CoffeeBot/Models/Session.swift @@ -0,0 +1,28 @@ +// +// Session.swift +// Session +// + +import Foundation +import BotsKit + +internal struct Session { + + var coffeesRequested : [CoffeeRequest] = [] + var membersReplied : [Account] { + get { + return self.coffeesRequested.map({ (request) -> Account in + return request.person + }) + } + } + + var description : String { + get { + let descriptions : [String] = self.coffeesRequested.map { (request) -> String in + return request.description + } + return descriptions.joined(separator: "\n") + } + } +} diff --git a/Sources/CoffeeBot/Services/AppointingService.swift b/Sources/CoffeeBot/Services/AppointingService.swift new file mode 100644 index 0000000..9b52561 --- /dev/null +++ b/Sources/CoffeeBot/Services/AppointingService.swift @@ -0,0 +1,35 @@ +// +// AppointingService.swift +// AppointingService +// +// Created by Said Ozcan on 28/06/2017. +// + +import BotsKit + +internal protocol AppointingServiceProtocol { + func appointJobToAPerson(with session:Session) -> Account +} + +internal class AppointingService { + + //MARK: Private + fileprivate func chooseAPerson(from persons:[Account]) -> Account { + let randomPersonIndex = generateRandomNumber(in: 0..) -> Int { + //TODO: Generate random number here + return 0 + } +} + +extension AppointingService : AppointingServiceProtocol { + + func appointJobToAPerson(with session: Session) -> Account { + //TODO: There could be some logic to not to choose previously chosen members maybe. + //Or choose the person who asks for a lot of things + return self.chooseAPerson(from: session.membersReplied) + } +} diff --git a/Sources/CoffeeBot/Services/EntityRecognitionService.swift b/Sources/CoffeeBot/Services/EntityRecognitionService.swift new file mode 100644 index 0000000..12481ed --- /dev/null +++ b/Sources/CoffeeBot/Services/EntityRecognitionService.swift @@ -0,0 +1,22 @@ +// +// EntityRecognitionService.swift +// PerfectLib +// +// Created by Said Ozcan on 28/06/2017. +// + +import Foundation + +internal protocol EntityRecognitionServiceProtocol { + func parse(_ text:String) -> [Coffee] +} + +internal class EntityRecognitionService {} + +extension EntityRecognitionService : EntityRecognitionServiceProtocol { + + func parse(_ text: String) -> [Coffee] { + //TODO: Parse entities in the given text and return relevant models + return [] + } +} diff --git a/Sources/CoffeeBot/Services/SessionService.swift b/Sources/CoffeeBot/Services/SessionService.swift new file mode 100644 index 0000000..b1738f8 --- /dev/null +++ b/Sources/CoffeeBot/Services/SessionService.swift @@ -0,0 +1,22 @@ +// +// SessionService.swift +// SessionService +// + +internal protocol SessionServiceProtocol { + func restoreSession() -> Session + func save(session:Session) +} + +internal class SessionService {} + +extension SessionService : SessionServiceProtocol { + + func restoreSession() -> Session { + return Session() + } + + func save(session: Session) { + + } +} diff --git a/Sources/CoffeeBot/Services/SpellCheckingService.swift b/Sources/CoffeeBot/Services/SpellCheckingService.swift new file mode 100644 index 0000000..8a5aace --- /dev/null +++ b/Sources/CoffeeBot/Services/SpellCheckingService.swift @@ -0,0 +1,18 @@ +// +// SpellCheckingService.swift +// SpellCheckingService +// + +internal protocol SpellCheckerServiceProtocol { + func normalize(_ text:String) -> String +} + +internal class SpellCheckingService {} + +extension SpellCheckingService : SpellCheckerServiceProtocol { + func normalize(_ text: String) -> String { + //TODO: Normalize the text via removing the symbols, numbers, irrelevant features, + //checking disambiguations and returning the normalized text + return text + } +} diff --git a/Sources/CoffeeBot/Services/TextProcessingService.swift b/Sources/CoffeeBot/Services/TextProcessingService.swift new file mode 100644 index 0000000..ebeda71 --- /dev/null +++ b/Sources/CoffeeBot/Services/TextProcessingService.swift @@ -0,0 +1,35 @@ +// +// TextProcessingService.swift +// TextProcessingService +// + +internal enum TextProcessingServiceResult { + case success([T]) + case error(Error) +} + +internal protocol TextProcessingServiceProtocol { + func process(_ text:String) -> TextProcessingServiceResult +} + +internal class TextProcessingService { + + //MARK: Properties + fileprivate let spellCheckerService : SpellCheckingService + fileprivate let entityRecognitionService : EntityRecognitionService + + //MARK: Lifecycle + init(spellCheckerService:SpellCheckingService, entityRecognitionService:EntityRecognitionService) { + self.spellCheckerService = spellCheckerService + self.entityRecognitionService = entityRecognitionService + } +} + +extension TextProcessingService : TextProcessingServiceProtocol { + + func process(_ text:String) -> TextProcessingServiceResult { + let normalizedString = self.spellCheckerService.normalize(text) + let coffeesRequested = self.entityRecognitionService.parse(normalizedString) + return TextProcessingServiceResult.success(coffeesRequested) + } +} diff --git a/Sources/CoffeeBot/spellchecker.swift b/Sources/CoffeeBot/spellchecker.swift deleted file mode 100644 index e3bd97a..0000000 --- a/Sources/CoffeeBot/spellchecker.swift +++ /dev/null @@ -1,44 +0,0 @@ - -import Foundation -import PerfectCURL - -func testFunc() -> String { - return "HueHue" -} - -class SpellCheker { - - internal func sendRequest(text: String, completion: @escaping (String?) -> (Void)) - { - do { - let body = self.textInBodyFormat(text: text) - let json = try CURLRequest("https://montanaflynn-spellcheck.p.mashape.com/check?text=" + body, .failOnError, - .httpMethod(CURLRequest.HTTPMethod.get), - .addHeader(.fromStandard(name: "X-Mashape-Key"), "OA5qHL58kvmshkz5xa4FQowNtD3tp1cD0n2jsnPY9TFf28l8Ka"), - .addHeader(.fromStandard(name: "Accept"), "application/json") - ).perform().bodyJSON - - let suggestion = self.suggestionFromJSON(json: json) - completion(suggestion) - } - catch let error { - fatalError("\(error)") - } - } - - private func textInBodyFormat(text: String) -> String - { - let result = text.replacingOccurrences(of: " ", with: "+") - return result - } - - private func suggestionFromJSON(json: [String:Any]) -> String? - { -// print("json: \(json)") - guard let result = json["suggestion"] as? String else { - return nil - } - return result - } -} -