Skip to content

Commit

Permalink
feat(thought-process): Add incoming stages and interfaces
Browse files Browse the repository at this point in the history
- Add thought process tests
- Update thought process docs
- Start Hubot migration docs
- Add .unloadListeners to clear listeners
- Clear listeners on bot reset
  • Loading branch information
timkinnane committed May 16, 2018
1 parent 3a9dd93 commit bd98c0d
Show file tree
Hide file tree
Showing 11 changed files with 301 additions and 47 deletions.
44 changes: 44 additions & 0 deletions md/HubotMigration.md
@@ -0,0 +1,44 @@
[thought]: ./ThoughtProcess.md

Requiring/importing the bot...

Async all the things

Logging and error handling

Coffee -> ES2015 -> Typescript

^^^

## Hearing and Listening

`listen`, `hear` and `respond` methods have counterparts within bBot but they
work slightly differently and we've attempted to find more semantic naming.

### Hear ➡️ ListenText
- In **Hubot** `hear` adds a text pattern listener.
- In **bBot** `listenText` does the same, where `hear` is the [process][thought]
which determines if incoming messages will be given to listeners.

### Respond ➡️ ListenDirect
- In **Hubot** `respond` add a text pattern listener that will only match if
prefixed with the bot's name.
- In **bBot** `listenDirect` does the same.

### Listen ➡️ ListenCustom
- In **Hubot** `listen` is a sort of abstract for both `hear` and `respond`.
- In **bBot** `listen` is the [process][thought] which provides messages to each
listener. `listenCustom` can be used to create a listener with a custom matching
function.

### Example

Hubot
```js
module.exports = (robot) => robot.hear(/.*/, () => console.log('I hear!'))
```

bBot
```js
export default (bot) => bot.listenText(/.*/, () => console.log('I listen!'))
```
13 changes: 8 additions & 5 deletions md/ThoughtProcess.md
Expand Up @@ -24,7 +24,7 @@ bBot takes in the message and if recognised, runs the scenario, furthering a
conversation or a simple exchange. It may not be immediately understood.

Listeners provide a matching function to evaluate the message and fire callbacks
on match. They are added with the following methods:
(or execute a named `bit`) on match. They are added with the following methods:

- `.listenText` adds regular expression matching on message text
- `.listenDirect` adds regular expressions prepended with the bot's name
Expand All @@ -44,7 +44,7 @@ A special type of `NaturalLanguageListener` is used for this stage, that
evaluates the intent and entities of the message, by sending the message to the
NLU adapter.

- `.understand` adds natural language matching on intent and/or entities
- `.understandText` adds natural language matching on intent and/or entities
- `.understandDirect` adds natural language that must be addressed to the bot
- `.understandCustom` adds custom natural language matching (given NLU result)

Expand All @@ -55,9 +55,12 @@ language listener, to interrupt or modify the state.

bBot takes any required action, locally or through external integrations.

This is an inherent result of the completion of `listen` and `understand`
middleware. Matched listeners will have their callbacks called, or if they
provided a `bit` key, those bits will be executed.
This is an implicit outcome of `listen` and `understand` stages, but if reached
without any listener matching, bBot creates a special `CatchAllMessage` message
to receive. Effectively restarting the process for a new set of listeners that
can take action when nothing else did.

- `.listenCatchAll` adds a callback for any unmatched message

## Respond.

Expand Down
4 changes: 3 additions & 1 deletion src/demo/listen-types.ts
Expand Up @@ -10,7 +10,9 @@ import * as bot from '..'
// Sends flowers to the console...
bot.setupBit({
id: 'send-flowers',
callback: (b: bot.B) => bot.logger.info(`💐 - ${b.listener.id} matched on "${b.message.toString()}"`)
callback: (b) => {
bot.logger.info(`💐 - ${b.listener.id} matched on "${b.message.toString()}"`)
}
})

// ...whenever someone says flowers
Expand Down
12 changes: 12 additions & 0 deletions src/lib/bot.spec.ts
Expand Up @@ -88,4 +88,16 @@ describe('bot', () => {
expect(bot.getStatus()).to.equal('started')
})
})
describe('.receive', () => {
it('enters hear process, executing middleware', async () => {
const callback = sinon.spy()
bot.load()
bot.hearMiddleware((b, next, done) => {
callback()
done()
})
await bot.receive(new bot.TextMessage(new bot.User(), 'test'))
sinon.assert.calledOnce(callback)
})
})
})
23 changes: 20 additions & 3 deletions src/lib/bot.ts
Expand Up @@ -10,6 +10,7 @@ import {
config,
name,
logger,
unloadListeners,
loadMiddleware,
unloadMiddleware,
loadAdapters,
Expand Down Expand Up @@ -122,18 +123,34 @@ export async function reset (): Promise<void> {
if (status !== 'shutdown') await shutdown()
unloadAdapters()
unloadMiddleware()
unloadListeners()
// unloadServer()
await eventDelay()
setStatus('waiting')
events.emit('waiting')
}

// Primary adapter interfaces...

/** Input message to put through thought process (alias for 'hear' stage) */
export function receive (message: Message, callback?: ICallback): Promise<B> {
return hear(message, callback)
}

/** Output message at end of thought process */
export async function send (): Promise<void> {
/** @todo */
/** Output message either from thought process callback or self initiated */
/** @todo Send via adapter and resolve with sent state */
export function send (message: Message, callback?: ICallback): Promise<B> {
console.log('"Sending"', message)
const b = new B({ message })
const promise = (callback) ? Promise.resolve(callback()) : Promise.resolve()
return promise.then(() => b)
}

/** Store data via adapter, from thought process conclusion or self initiated */
/** @todo Store via adapter and resolve with storage result */
export function store (data: any, callback?: ICallback): Promise<any> {
console.log('"Storing"', data)
const result = {}
const promise = (callback) ? Promise.resolve(callback()) : Promise.resolve()
return promise.then(() => result)
}
13 changes: 12 additions & 1 deletion src/lib/listen.spec.ts
Expand Up @@ -116,7 +116,7 @@ describe('listen', () => {
})
it('consecutive listeners share state changes', async () => {
listener.force = true
middleware.register((b: B) => {
middleware.register((b) => {
b.modified = (!b.modified) ? 1 : b.modified + 1
})
await listener.process(b, middleware)
Expand Down Expand Up @@ -273,4 +273,15 @@ describe('listen', () => {
sinon.assert.calledOnce(callback)
})
})
describe('.unloadListeners', () => {
it('clears all listeners from collection', () => {
listen.listenCatchAll(() => null)
listen.listenText(/.*/, () => null)
listen.understandText({}, () => null)
listen.understandCustom(() => null, () => null)
listen.unloadListeners()
expect(listen.listeners).to.eql({})
expect(listen.nluListeners).to.eql({})
})
})
})
6 changes: 6 additions & 0 deletions src/lib/listen.ts
Expand Up @@ -28,6 +28,12 @@ export const nluListeners: {
[id: string]: NaturalLanguageListener | CustomListener
} = {}

/** Clear current listeners, resetting initial empty listener objects */
export function unloadListeners () {
for (let id in listeners) delete listeners[id]
for (let id in nluListeners) delete nluListeners[id]
}

/** Interface for matcher functions - resolved value must be truthy */
export interface IMatcher {
(input: any): Promise<any> | any
Expand Down
6 changes: 5 additions & 1 deletion src/lib/middleware.ts
Expand Up @@ -20,7 +20,11 @@ export const middlewares: { [key: string]: Middleware } = {}
* done will be assumed.
*/
export interface IPiece {
(state: B, next: (done?: IPieceDone) => Promise<void>, done: IPieceDone): Promise<any> | void
(
state: B,
next: (done?: IPieceDone) => Promise<void>,
done: IPieceDone
): Promise<any> | any
}

/**
Expand Down
6 changes: 3 additions & 3 deletions src/lib/state.ts
Expand Up @@ -5,12 +5,12 @@ import * as bot from '..'
/**
* States accept some known common properties, but can accept any key/value pair
* that is needed for a specific type of listener or middleware.
* Will always be created with at least a message object.
* The `done` property tells middleware not to continue processing state.
*/
export interface IState {
message: bot.Message
done?: boolean
message?: bot.Message
listener?: bot.Listener
[key: string]: any
}

Expand All @@ -29,8 +29,8 @@ export class B implements IState {
[key: string]: any
constructor (startingState: IState) {
// Manual assignment of required keys is just a workaround for type checking
this.listener = startingState.listener
this.message = startingState.message
this.listener = startingState.listener
for (let key in startingState) this[key] = startingState[key]
}

Expand Down

0 comments on commit bd98c0d

Please sign in to comment.