Skip to content

Bots: Add metadata scheme for bots. #66

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

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

neiljp
Copy link
Contributor

@neiljp neiljp commented Aug 15, 2017

  • Name & (short) description
  • Whether to use any default commands (default to enabled):
    (about, '' (empty), usage [remove?], help/commands)
  • Provide a list of user commands, which help/commands uses

This is an alternative/supplement/extension of #58, which would also resolv #55.

I also raised the idea of bot metadata in #integrations.

Currently this just provides default-commands which use the simple metadata and (optionally) a list of bot commands provided by the developer. It could allow the user to provide callbacks directly, with the handle_message in the bot only being used for more complex input (like zulip/zulip#5047)

Copy link
Collaborator

@roberthoenig roberthoenig left a comment

Choose a reason for hiding this comment

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

This will be great! Left a couple stylistic comments. Oh, and there are some linter errors ;)

@@ -18,6 +18,11 @@ class WikipediaHandler(object):
kind of external issue tracker as well.
'''

META = {'name': 'Wikipedia',
'description': 'Searches Wikipedia for a term and returns the top article.',
'no_defaults': True, # Let bot handle all messages
Copy link
Collaborator

Choose a reason for hiding this comment

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

'defaults' might suit here as a name, to avoid negation in parameter names.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Amended, pending push.

@@ -6,6 +6,8 @@
XKCD_TEMPLATE_URL = 'https://xkcd.com/%s/info.0.json'
LATEST_XKCD_URL = 'https://xkcd.com/info.0.json'

from collections import OrderedDict
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: should this go to the other import statements?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Amended, pending push.

('usage', ((lambda: message_handler.usage(), "Bot-provided usage text"))),
('help', (lambda: "{}\n{}\n{}".format(def_about(), message_handler.usage(), def_help()),
"This help text")),
('commands', (def_commands, "A short list of supported commands"))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Hrmm, this construction is fun, but looks a little brittle.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The outer construction is the form of the OrderedDict constructor, the value is simply lambda+Text (so tuple seems ok?); are you referring to the formatting, the data-structure, or some other aspect?

for k, v in bot_details['commands'].items() if k != ''))
def def_commands():
return "**Commands**: {} {}".format(
" ".join(k for k in default_commands if k != ''),
Copy link
Collaborator

Choose a reason for hiding this comment

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

if k suffices.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Same for the other occurrences.

def def_about():
desc = bot_details['description']
return "**{}**{}".format(bot_details['name'],
"" if desc == "" else ": {}".format(desc))
Copy link
Collaborator

Choose a reason for hiding this comment

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

return "**{}**{}".format(bot_details['name'], not desc or ": {}".format(desc)) ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Interesting construction; I just played with this but it doesn't seem to work in practice as "not desc" gives True :)

Alternatively, just:

        if bot_details['description'] == "":
            return "**{}**".format(bot_details['name'])
        return "**{}**: {}".format(bot_details['name'], bot_details['description'])

?

])
# Update bot_details from those in class, if present
try:
bot_details.update(lib_module.handler_class.META)
Copy link
Collaborator

Choose a reason for hiding this comment

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

bot_details.update(getattr(lib_module.handler_class, 'META'. {}) ?

for command in default_commands:
if command == '':
continue
if message['content'].startswith(command):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe we want a simple equality check here, so we can simplify the whole # Handle any default_commands first if clause?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

To clarify, do you mean something like if default_commands == {}:, or even if default_commands: ?

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, I think we want an equality check, not startswith. It also means '' doesn't need to be a special case.

return "**Commands**: {} {}".format(
" ".join(k for k in default_commands if k != ''),
" ".join(k for k in bot_details['commands'] if k != ''))
default_commands = OrderedDict([
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why does this need to be an ordered dict?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If these are not ordered, then when iterated in the commands and help text, the order is not guaranteed - perhaps not vital for regular use, but tests won't be consistent either!

del default_commands[command]
# Sync default_commands changes with bot_details
if len(default_commands) == 0:
bot_details['no_defaults'] = True
Copy link
Collaborator

Choose a reason for hiding this comment

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

I've not entirely looked through this bot_detail default_commands code logic. Maybe this would be easier to understand if we outsource it to a separate function?

@neiljp
Copy link
Contributor Author

neiljp commented Aug 16, 2017

Updated to reflect most review comments; pending are final if statement and linter (since code has changed).

('latest', "Show the latest comic strip"),
('random', "Show a random comic strip"),
('<comic id>', "Show a comic strip with a specific 'comic id'"),
]) # NOTE: help not listed here, so default command used
Copy link
Member

Choose a reason for hiding this comment

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

I don't understand this comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In this bot, with this PR, help is now handled internally by the lib, so the default command is used instead as it stands. If 'help' was listed in commands then the bot wouldn't handle it and the help text would be as specified in the tuple.

return defaults

def sync_botdetails_defaultcommands(bot_details, default_commands):
# Update default_commands from any changes in bot_details
Copy link
Member

Choose a reason for hiding this comment

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

I don't understand this function; is it just "updating" in the sense that it's overriding the global defaults with this particular bot's settings?

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 broke out the code which adjusted default_commands based on bot_details (in some cases), but also have the last conditional which set default_commands_enabled. That's only (currently) used in that function, so could be cut, but that's why I prefixed it with 'sync'.

META = {
'name': 'Wikipedia',
'description': 'Searches Wikipedia for a term and returns the top article.',
'defaults': False, # Let bot handle all messages
Copy link
Member

Choose a reason for hiding this comment

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

I think the fact that you feel the need for a comment here suggests we might want a more verbose name than "defaults". I think "default_commands_enabled" would be a good name.

def def_about():
if bot_details['description'] == "":
return "**{}**".format(bot_details['name'])
return "**{}**: {}".format(bot_details['name'], bot_details['description'])
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't feel particularly readable. A better approach might be to write these as as e.g.

"**{name}**: {description}".format(**bot_details)

and probably even better would be e.g.

`"%(name)s: %(description)s" % bot_details

return ("\n".join("**{}** - {}".format(k, v[1])
for k, v in defaults.items() if k) + "\n" +
"\n".join("**{}** - {}".format(k, v)
for k, v in bot_details['commands'].items() if k))
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 use more verbose variable names than k/v here. I think we want "command_name" and "command_description".

I think some of the other new functions in this file could use the same sort of work.

As a general rule, one should basically always be verbose in variable names. Brevity doesn't provide much value, and the extra clarity is huge.

Also, put the newline on its own line so it is more visible.

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've generally been wary of verbosity here (not my code), perhaps because I'm aware how verbose I've been in my own code! (and I've generally gone very verbose, and wanted to fit in shortish lines here).
  • I've gone for variables without the command_ prefix, which I think provides a balanced approach.
  • The newline was on it's own line in a previous version; moved it back :)

('about', (def_about, "The type and use of this bot")),
('usage', ((lambda: message_handler.usage(), "Bot-provided usage text"))),
('help', (lambda: "{}\n{}\n{}".format(def_about(), message_handler.usage(), def_help()),
"This help text")),
Copy link
Member

Choose a reason for hiding this comment

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

I think the lambdas here aren't super readable; I'd rather those have an actual def method like the others.

del default_commands[command]
# Sync default_commands changes with bot_details
if len(default_commands) == 0:
bot_details['defaults'] = False
Copy link
Member

Choose a reason for hiding this comment

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

This function would be much better as something that returned a new dictionary based on the inputs, rather than mutating the default_commands dict. In addition to being a cleaner flow, I'm pretty sure the mutation means this code ends up being broken if multiple bots with different command sets run in the same process.

@neiljp neiljp force-pushed the default_commands branch 2 times, most recently from ccb9841 to b775c48 Compare August 23, 2017 01:18
@neiljp
Copy link
Contributor Author

neiljp commented Aug 23, 2017

@timabbott Changes made in a new commit to show the changes separately, though will squash or split as necessary.

@zulipbot
Copy link
Member

Hello @neiljp, it seems like you have referenced an issue in your pull request, but you have not referenced that issue in your commit message(s). When you reference an issue in a commit message, it automatically closes the corresponding issue when the commit is merged.

Please run git commit --amend in your command line client to amend your commit message description with "Fixes #[issue number]”.

An example of a correctly-formatted commit:

commit fabd5e450374c8dde65ec35f02140383940fe146
Author: zulipbot
Date:   Sat Mar 18 13:42:40 2017 -0700

    pull requests: Check PR commits reference when issue is referenced.

    Fixes #51

Thank you for your contributions to Zulip!

@neiljp
Copy link
Contributor Author

neiljp commented Aug 27, 2017

Current status is: (copying from zulip, to keep the context)

  • I've mostly updated the metadata PR, and put the changes into a separate commit to be squashed in the final version, if that works.
  • The updated (simpler) updated_default_commands approach @timabbott hinted at leaves the functions (were lambdas) pointing at the old data (notably for help & commands), so listing them in commands actually shows them twice (old default_commands and bot-provided commands list). This could be solved by going back to one data structure, if that could work, or some other approach involving passing in default_commands (new version) to the functions? (the alternative solution would be to just make default commands be all-or-nothing, ie. default_commands_enabled = False and commands is ignored, or True and they're used?)

@roberthoenig
Copy link
Collaborator

@neiljp I find this rather hard to review - it'd be awesome if you could split this in multiple, easily digestible commits; e.g. one for updating a bot with the feature, one for adding default commands, one for the core update command logic, etc.

Additionally, I think this would really profit from some unittests, which could for example be added in the updated bots themselves.

@zulipbot
Copy link
Member

Hello @neiljp, it seems like you have referenced an issue in your pull request, but you have not referenced that issue in your commit message(s). When you reference an issue in a commit message, it automatically closes the corresponding issue when the commit is merged.

Please run git commit --amend in your command line client to amend your commit message description with "Fixes #[issue number]”.

An example of a correctly-formatted commit:

commit fabd5e450374c8dde65ec35f02140383940fe146
Author: zulipbot
Date:   Sat Mar 18 13:42:40 2017 -0700

    pull requests: Check PR commits reference when issue is referenced.

    Fixes #51

Thank you for your contributions to Zulip!

@zulipbot
Copy link
Member

Hello @neiljp, it seems like you have referenced an issue in your pull request, but you have not referenced that issue in your commit message(s). When you reference an issue in a commit message, it automatically closes the corresponding issue when the commit is merged.

Please run git commit --amend in your command line client to amend your commit message description with "Fixes #[issue number]”.

An example of a correctly-formatted commit:

commit fabd5e450374c8dde65ec35f02140383940fe146
Author: zulipbot
Date:   Sat Mar 18 13:42:40 2017 -0700

    pull requests: Check PR commits reference when issue is referenced.

    Fixes #51

Thank you for your contributions to Zulip!

@zulipbot
Copy link
Member

Hello @neiljp, it seems like you have referenced an issue in your pull request, but you have not referenced that issue in your commit message(s). When you reference an issue in a commit message, it automatically closes the corresponding issue when the commit is merged.

Please run git commit --amend in your command line client to amend your commit message description with "Fixes #[issue number]”.

An example of a correctly-formatted commit:

commit fabd5e450374c8dde65ec35f02140383940fe146
Author: zulipbot
Date:   Sat Mar 18 13:42:40 2017 -0700

    pull requests: Check PR commits reference when issue is referenced.

    Fixes #51

Thank you for your contributions to Zulip!

@neiljp
Copy link
Contributor Author

neiljp commented Sep 30, 2017

I've refactored some of these commits to be smaller and clearer, and added some which amend the testing framework and zulip_bot_output.py to use the same logic as lib.py (before those commits, they provide different results from tests and running them, which indicates they should be in the same commit?).

I still would like to think/discuss about the way commands might work, but this is getting closer, I feel.

@neiljp
Copy link
Contributor Author

neiljp commented Sep 30, 2017

Currently the CI is failing, since I'm unsure whether to encode the complex default command responses in the tests, when they're so automated from the other data? That is, should I update the "help" text for the xkcd bot to ensure it passes, or remove that test?

@@ -168,6 +168,14 @@ def is_private_message_from_another_user(message_dict, current_user_id):
return current_user_id != message_dict['sender_id']
return False

def get_bot_details(bot_class, bot_name):
Copy link
Contributor

@rht rht Oct 1, 2017

Choose a reason for hiding this comment

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

LGTm for the commit ffc977a

@@ -168,10 +170,30 @@ def is_private_message_from_another_user(message_dict, current_user_id):
return current_user_id != message_dict['sender_id']
return False

def setup_default_commands(bot_details, message_handler):
Copy link
Contributor

Choose a reason for hiding this comment

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

I suppose this is the nucleus of the PR.

@zulipbot
Copy link
Member

zulipbot commented Oct 8, 2017

Hello @neiljp, a Zulip maintainer reviewed this pull request over 7 days ago, but you haven't updated your pull request since. Please take a look at the requested changes and update your pull request accordingly.

Thank you for your valuable contributions to Zulip!

@showell
Copy link
Contributor

showell commented Nov 6, 2017

Hi @neiljp Can you resolve the conflicts here?

@zulipbot
Copy link
Member

Heads up @neiljp, we just merged some commits that conflict with the changes your made in this pull request! You can review this repository's recent commits to see where the conflicts occur. Please rebase your feature branch against the upstream/master branch and resolve your pull request's merge conflicts accordingly.

@PIG208
Copy link
Member

PIG208 commented Jun 14, 2021

The concepts here are pretty valuable, but the PR has become too stale. I think we can close it but it's definitely worth doing a follow-up on this.

@neiljp
Copy link
Contributor Author

neiljp commented Jun 15, 2021

@PIG208 This PR is very old now and out of date, but my recollection was of various driving elements:

  • Using the META tags to be able to not just know the user-name for a bot, but what bot code is running (if there is) behind it - by whatever method; some bots already have these added
  • Having a layer on top of the bots to make a standard set of commands, or at least simple ways of interacting with bots, easier; so eg. help, about, ... and these could use the META tags (or not)

@neiljp
Copy link
Contributor Author

neiljp commented Jun 15, 2021

Note that #169 takes forward one of these points forward.

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

Successfully merging this pull request may close these issues.

7 participants