Comments (8)
The solution proposed for context is simple and well thinked, but I think it is too much responsability into one single function (a view). Also, the view for a while will take all control of the flow, and all error control, security, and wathever core stuff implemented in bottery could by bypassed in the view, possibly leading to bad implementations. The "head_set" implementation could avoid this problems, needs to be studied.
I've been thinking about a solution where the view returns a second parameter (True or False) that would mean: I want a "Hook" or "I want a conversation". The Pattern associated with the view would then process that parameter, and flag HimSelf to a special Pattern that manages the "hook". Looks more complicated, but the implementation of the view would be more simple. In this case, the context could be easily passed to another view, like in webapps, without having to bypass/disallow the central routing controller.
I have made a suggestion code. But since it is not the Pattern that calls the view, it just stores it, in the actual architeture this aproach would not work. It needs some change, that is simple: when bottery core retrieves the view from the Pattern, it is the Pattern object responsability to "run" the view.
class HookableFuncPattern(Pattern):
'''Receives a function to preprocess the incoming message
text before comparing it to the pattern.
Allows use of regular expressions, selecting partial words for
routing, etc
pre_process: a function to process message on check action before comparing
with pattern
context: string with history of messages
conversation: HookPattern Object that will hook any next messages to this pattern
(see ConversationPattern)'''
def __init__(self, pattern, view, pre_process, \
hook_pattern=None, save_context=True):
self.pre_process = pre_process
self.context = ""
self.conversation = hook_pattern
self.save_context = save_context
Pattern.__init__(self, pattern, view)
def call_view(self, message):
'''Local function to check return of view.
Just to treat errors if view returns only response'''
tuple_return = self.view(message)
if type(tuple_return) is tuple:
response = tuple_return[0]
hook = tuple_return[1]
else:
response = tuple_return
hook = False
return response, hook
def check(self, message):
''' If a view wants to begin a conversation, it needs to return True
Default is False.
First we see if the context has to be set, then we run the view.
While view returns True, the hook will remain'''
# If hooked, go directly to view
if (not self.conversation is None) and self.conversation.has_hook:
if self.save_context:
message.text = self.context + message.text
response, hook = self.call_view(message)
if not hook:
self.conversation.end_hook()
return response
# Else, begin normal check
text, _ = self.pre_process(message.text)
if text == self.pattern:
response, hook = self.call_view(message)
if hook:
self.context += text
if (not self.conversation is None) and (not self.conversation.has_hook):
self.conversation.begin_hook(self)
return response
return False
class HookPattern(Pattern):
'''FirstPattern to be checked. Allows a Pattern to "capture" and release
the flow if it receives an incomplete messsage
_pattern = a Pattern Object
Usage:
Put as first pattern
On a view, call set_conversation(Pattern) to ensure the next message will go to this Pattern
Also on a view, call end_conversation to release the hook'''
def __init__(self):
self._pattern = None
Pattern.__init__(self, "", None)
def check(self, message):
if self._pattern is None:
return False
return self._pattern.check(message)
def begin_hook(self, apattern):
'''Pass the pattern that will begin a conversation'''
self._pattern = apattern
def end_hook(self):
'''Releases pointer to Pattern ending a conversation'''
self._pattern = None
def has_hook(self):
'''Return if hook is active'''
return self._pattern is None
conversation = HookPattern()
patterns = [
conversation,
from bottery.
This is the part of the core code that needs to be changed:
` async def message_handler(self, data):
message = self.build_message(data)
# Try to find a view (best name?) to response the message
view = discover_view(message)
if not view:
return
response = view(message)`
from bottery.
Altough the 'hook' may seen complicated at first point, it allows more complex interactions. Let's say we build a bot that has a general mode, that can enter a command mode, two or more NLP modes, a query mode, and so on. The "command" mode should also act like a menu, having levels and/or asking for completion of parameters passed. Soon it would be impossible to decide what patterns go for each side. All the logic would be in the active view, and all control on it. This view could use NLP and other functions from bottery and other libs of the app, but the code could easily become a chain of crossing calls.
With 'hooks' maybe this would be simplier and we could even switch from one mode to other if the user wants, saving context for every "mode". And we would have a central point to see if the user wants to stay on this view/mode or not. Seems more organized at first glance.
Sorry that part of the example code became bad formatted on this forum. All the codes are on my github if it helps.
from bottery.
What do you think about a class
approach like: @nicoddemus
class PurcharseConversation(ConversationView):
def show_product_categories(self):
self.categories = get_product_categories()
return 'Thanks for shopping! Here are our product categories: {self.categories}'
def validate_category(self, resp):
return False if resp not in self.categories else True
def bail_out(self, resp):
return True if resp is 'I want to bail out' else False
def reject_category(self):
return 'Sorry, I do not recognize this category. Please select from: {self.categories}'
def end(self):
self.super().end('OK, sorry to see you go!')
async def start(self):
final_resp = await self.super().chain()
.ask(self.show_product_categories)
.while(self.validate_category)
.if(bail_out, self.end_conversation)
.do(self.reject_category)
return final_resp
async def purchase(message):
p = PurcharseConversation()
finished = await p.start()
return finished
}
patterns = [
Pattern('I want to buy stuff', purchase),
Pattern('Give me goods!', purchase),
]
from bottery.
A guy from work recommended RasaHQ https://github.com/RasaHQ/rasa_core.
from bottery.
I wrote a code that can handle a cli conversation following a simple dict configuration and make a request to an JSON API. I made tests on an application on my site and on a CEP WebService. The patterns.py code would be as simpler as follows. Running example on https://github.com/IvanBrasilico/bottery/tree/rules_tests. Just need a bot of yours in setting.py to test.
`rules = {'tec': {'rank': 'http://brasilico.pythonanywhere.com/_rank?words=',
'filtra': 'http://brasilico.pythonanywhere.com/_filter_documents?afilter=',
'capitulo': 'http://brasilico.pythonanywhere.com/_document_content/',
'_message': 'Informe o comando: '
}
}
rules_cep = {'cep': {'busca': 'http://api.postmon.com.br/v1/cep/',
'_message': 'Informe o comando: '
}
}
conversation = HookPattern(END_HOOK_LIST)
patterns = [
conversation,
Pattern('ping', pong),
Pattern('help', help_text),
HookableFuncPattern('tec', access_api_rules, two_tokens, conversation, rules=rules),
HookableFuncPattern('cep', access_api_rules, two_tokens, conversation, rules=rules_cep),
DefaultPattern(say_help)
]`
from bottery.
What do you think about a class approach lik
Using a class (or set of classes) is a perfectly valid approach. My example was meant just to illustrate the concept.
A guy from work recommended RasaHQ https://github.com/RasaHQ/rasa_core.
Wow that is awesome.
But I now realize that perhaps my example came across to implement full blown conversation to the bot, which was not my intent. My bad!
The await
/head_set
idea (that was a tongue in cheek name, probably a more suitable name would be conversation
or some other synonym) was meant to show the use-case where you want to ask some more information from the user before proceeding.
Here is a more concrete example of what I meant by this idea:
async def trigger_jenkins_job(message, conversation):
mask = await conversation.send('Got it. Which branch you want? (you can use wildcards)')
jobs = await fetch_jenkins_jobs(mask)
question = ['I found these:']
question += [f'{index} - {name}' for index, name in jobs]
question.append('Which one do you want to trigger?')
selected_index = await conversation.send('\n'.join(question))
selected_name = jobs[selected_index]
eta = trigger_jenkins_job(selected_name)
return f'Job {selected_name} has started! ETA: f{eta}, I will let you know when it finishes.'
patterns = [
Pattern('trigger job', trigger_jenkins_job),
]
This conversation would go like this:
> trigger job
Got it. Which branch you want? (you can use wildcards)
> *fix-flow*
I found these:
0. fb-fix-flow-solution-win64-py27
1. fb-fix-flow-solution-win64-py35
2. fb-fix-flow-solution-linux64-py27
3. fb-fix-flow-solution-linux64-py35
Which one do you want to trigger?
> 1
Job fb-fix-flow-solution-win64-py35 has started! ETA: 15:35, I will let you know when it finishes.
Without having the ability of sending new messages in the middle of the conversation, I have to remember some context myself somewhere because I will need to implement separate views. This can be done as:
async def trigger_job(message):
index = parse_job_mask(message)
if index is None:
return 'Missing branch index. Use the "search <mask>" command.'
jobs = await fetch_last_search()
selected_name = jobs[index]
eta = trigger_jenkins_job()
return f'Job {selected_name} has started! ETA: f{eta}, I will let you know when it finishes.'
async def search_mask(message):
mask = parse_job_mask(message)
if mask is None:
return 'Missing mask'
jobs = await fetch_jenkins_jobs(mask)
save_last_search(jobs)
found = ['I found these:']
found += [f'{index} - {name}' for index, name in jobs]
return '\n'.join(found)
patterns = [
Pattern('trigger job', trigger_job),
Pattern('search', search_mask),
]
The conversation:
> trigger job
Missing branch index. Use the "search <mask>" command.
> search *fix-flow*
I found these:
0. fb-fix-flow-solution-win64-py27
1. fb-fix-flow-solution-win64-py35
2. fb-fix-flow-solution-linux64-py27
3. fb-fix-flow-solution-linux64-py35
> trigger job 1
Job fb-fix-flow-solution-win64-py35 has started! ETA: 15:35, I will let you know when it finishes.
It of course works, but is less natural. But it would be awkward to try to get the user to confirm the job before triggering in this case (we don't know how long it has been since the last search).
So my point is that having the conversation
object makes some simple back and forth easier and more straightforward.
from bottery.
Hi.
As a suggestion, I implemented 3 different bottery "extensions". All of them use a "Hang" to capture the messages and a ContextHandler to maintain context information and user inputs. All of them have operational examples inside.
https://github.com/IvanBrasilico/bcontext - Just the ContextHandler, the view does all flow control.
https://github.com/IvanBrasilico/binput - Includes a "Input" command
https://github.com/IvanBrasilico/bdicttalk - Includes a command line processor. Allows to map a REST API, for example.
And also an app: https://github.com/IvanBrasilico/alfbot2 - just to map some JSON of pet apps tests of my site.
My option to use a "Hang" is to use the main loop to view communication, and not start another request to telegram (or other) outside of main async event loop. Since we dont have control of the loop, I think starting another request may led to conflicts, and, even it not, there are two possible situations:
Another pattern on the main loop consuming the waited message of the view.
The user simply ends the conversation, and the view will be stalled on the request.
Although operational, the code needs some improvement. There's need for refactoring, improving usage, making async requests, etc. But I think it can be a starting point, especially the "extension" approach, that does not bloat the core.
from bottery.
Related Issues (20)
- Create Issue and Pull Request templates
- Use pytest-deadfixtures on CI HOT 1
- View should be able to not response any message
- Middlewares
- Issue and Pull Request templates should not use markdown features
- Middleware example should be async HOT 3
- Warnings at test_bottery.test_default_properties
- Handler instances should returns the selected view
- Messenger considering wrong payload from Facebook webhook
- Move platforms modules outside platform module
- Allow hot-reload HOT 1
- Add Python 3.7 to tox.ini HOT 2
- Remove module dependency from settings HOT 1
- Create option on settings to change default module for msg handlers
- Remove `settings_module` param of Bottery
- Warnings: coroutine 'ClientSession.close' was never awaited
- Is there any roadmap for this project? HOT 1
- Add tox to the dev dependencies HOT 1
- Handle Telegram API response in case of failure
- Fix regex strings in tests and docs
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from bottery.