Skip to content
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

Agents endpoints #449

Merged
merged 46 commits into from
Sep 12, 2017
Merged

Agents endpoints #449

merged 46 commits into from
Sep 12, 2017

Conversation

PHLF
Copy link
Contributor

@PHLF PHLF commented Jun 28, 2017

Proposed changes:

Status:

  • ready for code review
  • there are tests for the functionality
  • documentation updated
  • changelog updated

The idea behind this is to enhance how RASA handles multiple apps to be on par with services like api.ai. Indeed when you have a client sending a training request like this 127.0.0.1:5000/train?name=agent_A you expect your agent (endpoint) to still be available for parsing requests and updated with the newly trained model once available. So models would be a way to version your agent:

  • agent_A
    • model_YYYYMMDD-HHMMSS (model actually in use)
    • model_YYYYMMDD-HHMMSS
    • model_YYYYMMDD-HHMMSS
    • ...
  • agent_B
    • model_YYYYMMDD-HHMMSS
    • ...

Original PR: #426

@PHLF PHLF mentioned this pull request Jun 28, 2017
6 tasks
@wrathagom
Copy link
Contributor

@PHLF am I correct that you haven't made any changes to the flask endpoints yet? I was curious what it would look like to load a specific agent /parse?q=hello&agent=myagent? How about a specific model of a specific agent? /parse?q=hello&agent=myagent&model=mymodel?

@PHLF
Copy link
Contributor Author

PHLF commented Jun 28, 2017

You're right: I didn't do any api changes at the moment. I'd like the actual work to be reviewed first before introducing any breaking changes. And yes loading a specific model for your agent is something that would be great to introduce 😄

@wrathagom
Copy link
Contributor

So without the ability to load a specific model, this would be a major breaking change for us. When we train a model we put it through an evaluation phase, only after the evaluation is done does a user in our system choose whether or not to activate it. The activation is done by maintain our own list of active models and simple routing request approprietly.

In the end we can hack it either way by just strictly interacting with agents as we have models. Just food for thought.

I'm swamped this week, but may get me or my team involved next week. Either way, we are watching with nervous anticipation.

@PHLF
Copy link
Contributor Author

PHLF commented Jun 28, 2017

You don't have to be nervous as my incentive is not to break anything but to remove the need of hacking things you mentioned. By the way it should be easy to extend the http api to request a specific model for a specific agent. Still this is just a PR with no guarantee to be integrated at all 😉

@PHLF PHLF mentioned this pull request Jun 28, 2017
6 tasks
Copy link
Member

@tmbo tmbo left a comment

Choose a reason for hiding this comment

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

Looking good 👍 some minor remarks

@@ -119,19 +115,19 @@ def test_post_parse_invalid_model(client, response_test):
assert response.json.get("error").startswith(response_test.expected_response["error"])


if __name__ == '__main__':
Copy link
Member

Choose a reason for hiding this comment

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

Should still have a main method to easily generate the models as these are models we don't want to retrain during the execution of the tests but rather test if we can still load previously trained models.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I call the train() function each time because I had an issue where the test_models trained with python 2 where not loadable with python 3 and vice versa because of pickle (the issue was more specifically related to joblib)

Copy link
Member

Choose a reason for hiding this comment

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

ok 👍

@staticmethod
def _latest_agent_model(agent_path):
"""Retrieves the latest trained model for an agent"""
agent_models = {model[6:]: model for model in os.listdir(agent_path)}
Copy link
Member

Choose a reason for hiding this comment

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

need to remove the magic number (e.g. define "model_" as a prefix constant somewhere and take its length here).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

return {
"trainings_under_this_process": num_trainings,
"available_models": models,
"available_models": agents,
Copy link
Member

Choose a reason for hiding this comment

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

these aren't models, right? ;)

Copy link
Contributor Author

@PHLF PHLF Jul 7, 2017

Choose a reason for hiding this comment

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

Right, this was left as is to be matter for discussion: what do we give to the end user?

  • Just the list of endpoints
  • The list of endpoints with the actual model
  • " " + the endpoints status {ready, training...} (would that be really useful)?

Copy link
Member

Choose a reason for hiding this comment

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

Can we actually get the information for the last one for all the endpoints (if easy that would be great if not its not a big deal)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What do you mean exactly? Are you talking about the endpoint status or the underlying model?

Copy link
Contributor Author

@PHLF PHLF Jul 23, 2017

Choose a reason for hiding this comment

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

How I see things:

{
	"agent A": {
		"status": "ready",
		"default_model": "model_xxxxxx",
		"available_models": [
			"model_xxxxxx",
			"model_xxxxxx"
		]
	},
	"agent B": {
		"status": "training",
		"default_model": "model_xxxxxx",
		"available_models": [
			"model_xxxxxx",
			"model_xxxxxx"
		]
	}
}

@wrathagom Would that be enough for your usecase?

@PHLF
Copy link
Contributor Author

PHLF commented Jul 7, 2017

@tmbo Do you have any suggestion regarding some http API changes: can I go for loading a specific model for a specific agent?

@tmbo
Copy link
Member

tmbo commented Jul 21, 2017

@PHLF sure that sounds very useful. The only thing we need then is a way for the API user to know which model currently is loaded for an agent.

@tmbo
Copy link
Member

tmbo commented Jul 21, 2017

And there might be threading issues, right? So you could have loaded the recent model within one thread and an older one in another thread, which sounds dangerous.

@PHLF
Copy link
Contributor Author

PHLF commented Jul 21, 2017

  1. That should be part of the /status endpoint response.
  2. I need to check that. Edit: the case you mentioned is not really an issue but I agree that changing a model which is being used by another thread for parsing is to be avoided.

@PHLF
Copy link
Contributor Author

PHLF commented Jul 23, 2017

Checks are erroring: that's OK. That's still very much W.I.P. but at least it is advanced enough to allow you to give some feedback about the current code structure (among other things, I need to take care of multithreading issues).

@PHLF
Copy link
Contributor Author

PHLF commented Sep 11, 2017

Agent -> Projects ? Sounds a bit strange to me. What is the motivation behind such a name ?

@amn41
Copy link
Contributor

amn41 commented Sep 11, 2017

So you might have multiple bots under development (say one restaurant bot and one customer service bot), and each has it's own training data and model. So the project would be e.g. 'restaurants', and you can always call /parse?project=restaurants and get the latest model

@PHLF
Copy link
Contributor Author

PHLF commented Sep 11, 2017

Fair enough. Let's do some renaming then.

@PHLF
Copy link
Contributor Author

PHLF commented Sep 11, 2017

Should the logic behind the emulators be changed? As they only allow to query a specific "project" therefore automatically querying the latest model (which is the behaviour of API.ai likes services).

@amn41
Copy link
Contributor

amn41 commented Sep 11, 2017

I think sensible behaviour would be to either specify a project (&rasa uses the latest) or specifying a specific model - @tmbo WDYT?

@tmbo
Copy link
Member

tmbo commented Sep 11, 2017

Yes, I agree, querying the latest is a sensible default. But being able to use a specific model is quite useful as well - should do both.

@PHLF
Copy link
Contributor Author

PHLF commented Sep 11, 2017

Querying a specific model for a specific project already works, but I'm not sure it works with the emulators as well.

@tmbo
Copy link
Member

tmbo commented Sep 11, 2017

sounds good. why wouldn't the emulation work? I think they are just post processing the pipeline result

@PHLF
Copy link
Contributor Author

PHLF commented Sep 11, 2017

My bad, it's not related to the emulators but to the way some parameters are normalized. Does this need further changes to also normalize a model parameter ?

    def normalise_request_json(self, data):
        # type: (Dict[Text, Any]) -> Dict[Text, Any]

        _data = {}
        _data["text"] = data["q"][0] if type(data["q"]) == list else data["q"]
        if not data.get("project"):
            _data["project"] = "default"
        elif type(data["project"]) == list:
            _data["project"] = data["project"][0]
        else:
            _data["project"] = data["project"]
        _data['time'] = data["time"] if "time" in data else None
        return _data

@tmbo
Copy link
Member

tmbo commented Sep 11, 2017

yes, that's right, the parameter needs to be added there.

@PHLF
Copy link
Contributor Author

PHLF commented Sep 12, 2017

I made some changes (readers-writer lock) to ease the development of further enhancements like an unload HTTP endpoint to delete unused models from memory.

@@ -32,23 +34,32 @@ def __init__(self, config=None, component_builder=None, project=None):
self._default_model = self._latest_project_model() or 'fallback'

def parse(self, text, time=None, model=None):
# Lazy model loading
# Readers-writer lock simple double mutex implementation
self._reader_lock.acquire()
Copy link
Member

Choose a reason for hiding this comment

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

let's put the lock handling into a separate function

self._lock.release()
response = self._models[model].parse(text, time)

self._reader_lock.acquire()
Copy link
Member

Choose a reason for hiding this comment

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

separate function

@tmbo
Copy link
Member

tmbo commented Sep 12, 2017

@PHLF do you think we can get this merged today?

@PHLF
Copy link
Contributor Author

PHLF commented Sep 12, 2017

I have to update the docs, but I think this is technically ready.

@PHLF
Copy link
Contributor Author

PHLF commented Sep 12, 2017

@amn41 @tmbo @wrathagom As I said I think this is technically OK. I did a first pass on the docs. Let me know what you think.

@tmbo
Copy link
Member

tmbo commented Sep 12, 2017

looks really good - you did a great job with this PR let's get it merged when the Travis build is finished. we'll tackle issues with the documentation along the way.

@PHLF
Copy link
Contributor Author

PHLF commented Sep 12, 2017

you did a great job with this PR

Only time will tell 😃

Next thing to add: an HTTP endpoint to unload models from memory. The server side logic is in place, it just requires a new endpoint and a new route handler in the datarouter.

Thanks Tom for your support.

@tmbo
Copy link
Member

tmbo commented Sep 12, 2017

Yes, you are right, I think that is another really good addition. But as I don't want to delay this PR any further (I know that you already needed to resolve a lot of merge conflicts 😉 ) I'd rather see them added separately.

@tmbo tmbo merged commit 3ef9663 into RasaHQ:master Sep 12, 2017
@tmbo
Copy link
Member

tmbo commented Sep 12, 2017

🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants