Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
48 changes: 48 additions & 0 deletions Sources/CoffeeBot/CoffeeBot.swift
Original file line number Diff line number Diff line change
@@ -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<Activity> = 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() {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can set dependencies of this class (SessionService, Appointng, etc) as parameters for init and provide default implementations as default value
public init(sessionService: SessionService = SessionService(), ...)
With this approach it's easy to write unit tests and inject dependecies.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you are right I will create a factory to create CoffeeBot, in this way we will be able to inject dependencies.
I would like to create own factory class because I think external classes don't necessarily know about all internal types such as EntityRecognitionService.

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)
}
}
94 changes: 94 additions & 0 deletions Sources/CoffeeBot/CoffeeManager/CoffeeManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
//
// CoffeeManager.swift
// SwiftBot
//

import Foundation
import BotsKit
import LoggerAPI

internal protocol CoffeeManagerProtocol {
func process(activity:Activity)
}

class CoffeeManager {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of this entity?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought of CofeeBot as a main.m which speaks with the external modules and this CoffeeManager handles all the internal services and everything.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I think you can merge this class to CoffeeBot. And then extend CoffeeBot with Bot protocol.
So you have entry point to your Bot and you can test it as you want and in same time extending from Bot protocol give you ability to integrate it inside BotsKit


//MARK: Properties
fileprivate var session : Session? = nil
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the bot will process few session in the same time?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there could be only one session at a time. Because if a second session is started before the first one is ended how will we differentiate between which coffee request came to which session?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good question :) Here is the difference between mobile app and backend, one app needs to process different requests at the same time.
Let's discuss this on next meeting!

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) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about close this Services by protocols?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is a really good point, I will think of this. I only have one concern which is:
If I receive something like init(services:[CoffeeBotServiceProtocol]) since all services have different functionality and method signatures I am not sure if I can unify them.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean create protocols for each service and then create implementation for each protocol, this can help you to tests your bot

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 {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point I think we need to add some timer pulling into BotsKit

//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)")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think good variant will ask user repeat his message

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)
}
}
}
18 changes: 18 additions & 0 deletions Sources/CoffeeBot/Models/Coffee.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// Coffee.swift
// Coffee
//

internal struct Coffee {
let amount : Int
let type : CoffeeType
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about internal type in Coffee namespace? Something like:
let type: Coffee.Type

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I never have done this before. Can you share an example?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, https://github.com/s/SwiftBot/blob/master/Sources/ChatProviders/Facebook/MessengerWebhook.swift#L72
or in your case:

internal struct Coffee {
    let amount : Int
    enum `Type`: String {
        case espresso
        case capuchino
    }
    let type: Type
}

Then you can extend your type with this:

extension Coffee.Type {
    func ...
}

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:" "))"
}
}
}
9 changes: 9 additions & 0 deletions Sources/CoffeeBot/Models/CoffeeAddition.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//
// CoffeeAddition.swift
// CoffeeAddition
//

public enum CoffeeAddition : String {
case milk
case sugar
}
19 changes: 19 additions & 0 deletions Sources/CoffeeBot/Models/CoffeeRequest.swift
Original file line number Diff line number Diff line change
@@ -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:" "))"
}
}
10 changes: 10 additions & 0 deletions Sources/CoffeeBot/Models/CoffeeType.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//
// CoffeeType.swift
// CoffeeType
//

internal enum CoffeeType : String {
case latte
case capuccino
case filterCoffee
}
28 changes: 28 additions & 0 deletions Sources/CoffeeBot/Models/Session.swift
Original file line number Diff line number Diff line change
@@ -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")
}
}
}
35 changes: 35 additions & 0 deletions Sources/CoffeeBot/Services/AppointingService.swift
Original file line number Diff line number Diff line change
@@ -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..<persons.count)
return persons[randomPersonIndex]
}

fileprivate func generateRandomNumber(in range:Range<Int>) -> 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)
}
}
22 changes: 22 additions & 0 deletions Sources/CoffeeBot/Services/EntityRecognitionService.swift
Original file line number Diff line number Diff line change
@@ -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 []
}
}
22 changes: 22 additions & 0 deletions Sources/CoffeeBot/Services/SessionService.swift
Original file line number Diff line number Diff line change
@@ -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) {

}
}
18 changes: 18 additions & 0 deletions Sources/CoffeeBot/Services/SpellCheckingService.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
35 changes: 35 additions & 0 deletions Sources/CoffeeBot/Services/TextProcessingService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
// TextProcessingService.swift
// TextProcessingService
//

internal enum TextProcessingServiceResult<T> {
case success([T])
case error(Error)
}

internal protocol TextProcessingServiceProtocol {
func process(_ text:String) -> TextProcessingServiceResult<Coffee>
}

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<Coffee> {
let normalizedString = self.spellCheckerService.normalize(text)
let coffeesRequested = self.entityRecognitionService.parse(normalizedString)
return TextProcessingServiceResult.success(coffeesRequested)
}
}
Loading