-
Notifications
You must be signed in to change notification settings - Fork 3
initial logic #34
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
initial logic #34
Changes from all commits
2149245
2b5c3b2
dab2370
1b3db3a
69c27c1
1cdea8f
d6b3971
955b33f
f730f35
cb3c88c
6623c4f
f10ff07
dadd20b
83d96bc
7313101
c9ca73f
dc74826
bbb0221
a8bc408
4661e5a
777e527
79da0f4
20f5f5e
4686a88
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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() { | ||
| 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) | ||
| } | ||
| } | ||
| 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 { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the purpose of this entity?
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
|
|
||
| //MARK: Properties | ||
| fileprivate var session : Session? = nil | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if the bot will process few session in the same time?
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| 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) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do you think about close this Services by protocols?
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)") | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
| } | ||
| } | ||
| } | ||
| 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 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do you think about internal type in Coffee namespace? Something like:
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, I never have done this before. Can you share an example?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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:" "))" | ||
| } | ||
| } | ||
| } | ||
| 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 | ||
| } |
| 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:" "))" | ||
| } | ||
| } |
| 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 | ||
| } |
| 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") | ||
| } | ||
| } | ||
| } |
| 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) | ||
| } | ||
| } |
| 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 [] | ||
| } | ||
| } |
| 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) { | ||
|
|
||
| } | ||
| } |
| 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 | ||
| } | ||
| } |
| 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) | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.