Coder Social home page Coder Social logo

Comments (8)

IvanBrasilico avatar IvanBrasilico commented on September 13, 2024

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.

IvanBrasilico avatar IvanBrasilico commented on September 13, 2024

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.

IvanBrasilico avatar IvanBrasilico commented on September 13, 2024

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.

guidiego avatar guidiego commented on September 13, 2024

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.

rougeth avatar rougeth commented on September 13, 2024

A guy from work recommended RasaHQ https://github.com/RasaHQ/rasa_core.

from bottery.

IvanBrasilico avatar IvanBrasilico commented on September 13, 2024

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.

nicoddemus avatar nicoddemus commented on September 13, 2024

@guidiego

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.

@rougeth

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.

IvanBrasilico avatar IvanBrasilico commented on September 13, 2024

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)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo 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.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.