Package: pyconversation
PyConversationZero-dependency library for chat-bot creators with deadlines.It allows you to describe a conversation, talk with user according to your schema and restore it, if something went wrong.Table of contentsQuickstartMessagesTextGroupAskSwitchListAskTerminateGroupCreating Own MessagesMessageTransferLoggersDictLoggerJsonFileLoggerCreating Own LoggersMessage SenderCompatibilityQuickstartFirst we need to create a message schema, which consists of messages.Messages describe chat-bot's actions. For instance, send a text message, which doesn't need any feedback or ask a question. Each message has a unique id. Most common types of messages areGroup,TextandAsk.For full details about different message typessee MessagesGroupis a kind of container, that holds list of other messages.Textsends a text message, which doesn't require any feedback.Asksends a text message and waits for an answerEnough theory, let's see an example!frompyconversationimportGroup,Text,Askfruit_bot_conversation=Group(id="root",children=[Text(id="root.hello",text="Hello!"),Ask(id="root.fruits",text="What fruits do you like?"),Text(id="root.bye",text="Bye"),],)In this example, we create a schema for simple bot, who asks which fruits does user like. Root message is aGroup. It holds a block of messages. First of them is aTextwhich sends user a greeting message. Second one (Ask) asks about user's favorite fruit and waits for answer. And finally, thirdTextmessage sendsByeto user.Second step - we need a loggerLogger is an object, which stores user's answers and message history. This library exposes 2 loggers:DictLogger- stores data in a dictionaryJsonFileLogger- takes file path as a parameter and stores json data in this fileIf you need something different,see Creating Own LoggersBut now let's useDictLoggerfrompyconversationimportDictLoggerlogger=DictLogger()That's all!For full loggers documentationsee LoggersBut how to send those messages?The answer's simple - using aMessageSender!Example code:frompyconversationimportGroup,Text,Ask,DictLogger,MessageSender# Conversation from step 1fruit_bot_conversation=Group(id="root",children=[Text(id="root.hello",text="Hello!"),Ask(id="root.fruits",text="What fruits do you like?"),Text(id="root.bye",text="Bye"),],)# Logger from step 2logger=DictLogger()# Initialize a message sendersender=MessageSender(root=fruit_bot_conversation,# Our conversationlogger=logger,# Our loggersend=print# A send function, which takes a string and sends the message. In this case, we use print to log messages to console)# Answer to the question before the first one is always emptyanswer=None# Send messages!whileTrue:# Send messages one by one, until we run into a message, which requires an answer# This function takes answer to previous question as a parametersender.send_all_skippable(answer)# If all messages sentifsender.finished:# Dispose of sender's resources (like open files) and get the result!print("\nResult:",sender.finalize())break# If not all messages have been sent and we still need an answer, ask!answer=input()Done! If you run this, you'll get the following in the console:Hello!
What fruits do you like?
<your answer'll be here>
Bye

Result: {'root.fruits': '<your answer>'}And one more example with decorated function (like in real chat-bots):bot=...# Initialize chat botsender=None@bot.connectiondefon_connection(user_id):sender=MessageSender(root=conversation,# Our conversationlogger=logger,# Our loggersend=lambdatext:bot.send(user_id,text))sender.send_all_skippable(None)@bot.messagedefon_message(user_id,message):sender.send_all_skippable(message)ifsender.finished:print("\nResult:",sender.finalize())sender=NoneFor full message sender documentationsee Message SenderYou've created your first chat-bot with clever conversation! Here quick tutorial ends.MessagesTextText message sends some text, which doesn't require user's answerConstructor parameters:id(str) - unique message idtext(str) - text to sendUsage example:Text(id="hello",text="Hello, user!")GroupGroup is a message, which doesn't send anything and doesn't require an answer. It's just a container for a list of messagesConstructor parameters:id(str) - unique message idchildren(list[message]) - list of messages to sendUsage example:Group(id="group",children=[Text(id="hello",text="Hello!"),Text(id="bye",text="Good bye!"),],)AskAsk message send some text to user and waits for an answerConstructor parameters:id(str) - unique message idtext(str) - question textUsage example:Ask(id="name",text="What's your name?")SwitchSwitch message asks user a question and sends a message depending on user's answer.Constructor parameters:id(str) - unique message idtext(str) - question textanswer_map(dict[str, message]) - dict, where key is user's answer and value is a messagefallback(message?) - message, which'll be sent if answer doesn't match anything inanswer_mapdictrepeat_on_fallback(bool?) - if true, after fallback was sent question is asked over and over again until answer matches something inanswer_mapdictUsage example:Switch(id="fruit"text="What fruit do you like?"answer_map={"apple":Text(id="apple",text="Yeah, apples are delicious!"),"peach":Text(id="peach",text="Me too!"),"feijoa":Text(id="feijoa",text="I don't know that fruit!"),},fallback=Text(id="dont_understand",text="Sorry, I didn't understand you"),repeat_on_fallback=True)ListAskListAsk asks user a question and waits for several answers.In result dictionary it's represented by an array.Constructor parameters:id(str) - unique message idtext(str) - question textstop_command(str) - if user sends this string as an answer, ListAsk finishes waiting for answersmax_count(int?) - maximal count of answersUsage example:ListAsk(id="fruits",text="What fruits do you like? Enter 'that's all' if you can't remember any more",stop_command="that's all",max_count=10,)TerminateGroupTerminateGroup sends another message and then terminates sending group, inside which it is locatedConstructor parameters:id(str) - unique message idchild(message?) - message to send before terminating the groupUsage example:Group(# This group's gonna be terminatedid="group",children=[Text(id="hello",text="Hello!"),Switch(id="bye_condition",text="Can I say bye?",answer_map={"yes":Text(id="bye",text="Good bye!")},fallback=TerminateGroup(id="terminate",child=Text(id="eh",text="Eh..."),),),Text(id="what",text="What?!"),# This will not be sent,],)Creating Own MessagesEvery message is a class, so to create your own message, you just need to inheritBaseMessageclass (It can be imported like this:from pyconversation import BaseMessage)Usage example:frompyconversationimportText,BaseMessage,BaseLogger,MessageTransfer,MessageTransferGeneratorclassHelloMessage(BaseMessage):username:strdef__init__(self,*,id:str,username:str)->None:super().__init__(id=id)# BaseMessage takes one parameter - idself.username=usernamedef_base_iterator(self,logger:BaseLogger)->MessageTransferGenerator:# This is an abstract methodtext_message=Text(id=f"{self.id}.text",text=f"Hello,{self.username}!")yield fromtext_message.iterator(logger)answer=yieldMessageTransfer(id=self.id,text="Is it your real name?",)logger.log(self.id,answer)As you can see, each message has an iterator method, which takes logger as a parameter and returns a generator. Also, this message gets an answer and logs it to logger. Details on how to interact with logger and log answers will be explained inLoggersBut what is thatMessageTransferobject? It's used to pass string message to sender and get an answer. Details in next article.MessageTransferMessage transfer is used to pass string message to sender and get an answer. It can beyielded from message's generator.Constructor parameters:id(str) - message's unique idtext(str?) - text, which'll be sent to user or None, if you don't want to ask any questions, you just need an answerskip(bool?) - if true, this question doesn't need an answer and won't wait for it.terminate_group(bool?) - when this is true, group which intercepted such transfer processes it and terminates.Usage example in upperCreating Own MessagessectionLoggersLoggers are used to store users' answers and message history.Message history is a list, where question ids are stored. It's used to restore conversation. For example, if user has already answered several questions and suddenly the server stops, last sent message id will be taken from history, and conversation will begin from the last message.DictLoggerDictLogger stores answers and history in-memory (in a dictionary). So it's just an example to play with the library. Don't use it in production code.No constructor parameters.Usage example:logger=DictLogger()JsonFileLoggerJsonFileLogger stores everything in a JSON file. JSON file stays on the computer anyway, so when server suddenly stops and the reboots, your bot'll be able to continue conversation from the right place.Constructor parameters:file_path(str) - JSON file's absolute path. It must be unique between all conversations on this server.Usage example:logger=JsonFileLogger(pathlib.Path(__file__).parent/"conversation.json")Creating Own LoggersIf you need to create your own logger (and you'll need it more often, than creating own messages) you need to inherit theBaseLoggerclass.It has the following abstract methods:log(-> None) - stores answer by message's unique idParameters:id(str) - message unique idvalue(str) - answerset_array(-> None) - initializes empty list in answer dictionary using message unique id as a keyParameters:id(str) - message unique idadd_array_item(-> None) - add item to existing list using message id as answer dictionary keyParameters:id(str) - message unique idvalue(str) - value to add to listget(-> union[str, list[str], None]) - get message answer or list of answers by message id if existsParameters:id(str) - message unique idget_result_dict(-> dict[str, union[str, list[str]]]) - get full answer dictionaryNo parametersAnd also the following virtual methods (not necessary to implement):reset_history(-> None) - remove all elements from message history listNo parameterslog_last_id(-> None) - add message id to message history listParameters:id(str) - message unique idget_last_id(-> str?) - get last sent message id (last element in message history list)No parametersfinalize(-> None) - dispose of logger's resources (open files, socket connections, etc.)Note: This method is called when the conversation is finished. So, for instance,JsonFileLoggerdeletes it's data file in this method.No parametersUsage example:fromtypingimportUnion,List,DictfrompyconversationimportBaseLoggerclassMySocketLogger(BaseLogger):socket:Socketdef__init__(self)->None:super().__init__()self._connect_socket()deflog(self,id:str,value:str)->None:self.socket.emit("SET_OR_REPLACE",{"id":id,"value":value})defset_array(self,id:str)->None:self.socket.emit("SET_OR_REPLACE",{"id":id,"value":[]})defadd_array_item(self,id:str,value:str)->None:self.socket.emit("ADD_ARRAY_ITEM",{"id":id,"value":value})defget(self,id:str)->Union[str,List[str]]:returnself.socket.emit("GET",{"id":id})defget_result_dict(self)->Dict[str,Union[str,List[str]]]:returnself.socket.emit("GET_ALL")defreset_history(self)->None:self.socket.emit("SET_HISTORY",[])deflog_last_id(self,id:str)->None:self.socket.emit("ADD_HISTORY",id)defget_last_id(self,id:str)->Union[str,None]:ifnotself.socket.emit("HISTORY_EMPTY"):returnself.socket.emit("GET_LAST_IN_HISTORY")deffinalize(self)->None:self.socket.emit("CLEAR_EVERYTHING")self._disconnect_socket()def_connect_socket(self)->None:self.socket=...# We'll log our data using a socketdef_disconnect_socket(self)->None:self.socket.disconnect()self.socket=NoneMessage SenderMessage sender is used to simplify conversation restoring and message sending.Constructor parameters:root(message) - root message (aka message schema)logger(logger) - loggersend(function (str) -> None) - send function (takes string and sends it to user)headline_text(str?) - text, which'll be sent to user whent message sender is constructed. Whether conversation is constructed or restored, it's sent anyway.stop_command(str?) - if user sends this as an answer, conversation terminates.Exposed properties:finished(bool) - is conversation finished (true if all messages have been sent or conversation has been stopped by stop command)terminated(bool) - is conversation terminated (true if conversation was stopped by stop command)Exposed methods:send_all_skippableSend all messages until sender runs into a message, which requires an answer.Parameters:prev_answer(str?) - answer to previous messageSee usage example inQuickstartCompatibilityThis library is compatible with Python>=3.6© 2021 Roman Melamud