コード例 #1
0
ファイル: test_trackers.py プロジェクト: zuiwanting/rasa
def test_reading_of_trackers_with_legacy_form_events():
    loop_name1 = "my loop"
    loop_name2 = "my form"
    tracker = DialogueStateTracker.from_dict(
        "sender",
        events_as_dict=[
            {
                "event": ActiveLoop.type_name,
                "name": loop_name1
            },
            {
                "event": LegacyForm.type_name,
                "name": None
            },
            {
                "event": LegacyForm.type_name,
                "name": loop_name2
            },
        ],
    )

    expected_events = [
        ActiveLoop(loop_name1),
        LegacyForm(None),
        LegacyForm(loop_name2)
    ]
    assert list(tracker.events) == expected_events
    assert tracker.active_loop["name"] == loop_name2
コード例 #2
0
    async def continue_training(request: Request):
        epochs = request.raw_args.get("epochs", 30)
        batch_size = request.raw_args.get("batch_size", 5)
        request_params = request.json
        sender_id = UserMessage.DEFAULT_SENDER_ID

        try:
            tracker = DialogueStateTracker.from_dict(sender_id, request_params,
                                                     app.agent.domain.slots)
        except Exception as e:
            raise ErrorResponse(400, "InvalidParameter",
                                "Supplied events are not valid. {}".format(e),
                                {
                                    "parameter": "",
                                    "in": "body"
                                })

        try:
            # Fetches the appropriate bot response in a json format
            app.agent.continue_training([tracker],
                                        epochs=epochs,
                                        batch_size=batch_size)
            return response.text('', 204)

        except Exception as e:
            logger.exception("Caught an exception during prediction.")
            raise ErrorResponse(500, "TrainingException",
                                "Server failure. Error: {}".format(e))
コード例 #3
0
ファイル: tracker_store.py プロジェクト: Robosensus1/rasa-1
    def retrieve(self, sender_id):
        """
        Args:
            sender_id: the message owner ID

        Returns:
            `DialogueStateTracker`
        """
        stored = self.conversations.find_one({"sender_id": sender_id})

        # look for conversations which have used an `int` sender_id in the past
        # and update them.
        if stored is None and sender_id.isdigit():
            from pymongo import ReturnDocument

            stored = self.conversations.find_one_and_update(
                {"sender_id": int(sender_id)},
                {"$set": {
                    "sender_id": str(sender_id)
                }},
                return_document=ReturnDocument.AFTER,
            )

        if stored is not None:
            return DialogueStateTracker.from_dict(sender_id,
                                                  stored.get("events"),
                                                  self.domain.slots)
        else:
            return None
コード例 #4
0
    async def replace_events(request: Request, conversation_id: Text):
        """Use a list of events to set a conversations tracker to a state."""
        validate_request_body(
            request,
            "You must provide events in the request body to set the sate of the "
            "conversation tracker.",
        )

        verbosity = event_verbosity_parameter(request,
                                              EventVerbosity.AFTER_RESTART)

        try:
            tracker = DialogueStateTracker.from_dict(conversation_id,
                                                     request.json,
                                                     app.agent.domain.slots)

            # will override an existing tracker with the same id!
            app.agent.tracker_store.save(tracker)
            return response.json(tracker.current_state(verbosity))
        except Exception as e:
            logger.debug(traceback.format_exc())
            raise ErrorResponse(
                500,
                "ConversationError",
                "An unexpected error occurred. Error: {}".format(e),
            )
コード例 #5
0
ファイル: tracker_store.py プロジェクト: suhaibmujahid/rasa
    def retrieve(self, sender_id: Text) -> Optional[DialogueStateTracker]:
        """Create a tracker from all previously stored events."""

        with self.session_scope() as session:
            query = session.query(self.SQLEvent)
            result = (
                query.filter_by(sender_id=sender_id)
                .order_by(self.SQLEvent.timestamp)
                .all()
            )

            events = [json.loads(event.data) for event in result]

            if self.domain and len(events) > 0:
                logger.debug(f"Recreating tracker from sender id '{sender_id}'")
                return DialogueStateTracker.from_dict(
                    sender_id, events, self.domain.slots
                )
            else:
                logger.debug(
                    f"Can't retrieve tracker matching "
                    f"sender id '{sender_id}' from SQL storage. "
                    f"Returning `None` instead."
                )
                return None
コード例 #6
0
    def retrieve(self, sender_id):
        stored = self.conversations.find_one({"sender_id": sender_id})

        # look for conversations which have used an `int` sender_id in the past
        # and update them.
        if stored is None and sender_id.isdigit():
            from pymongo import ReturnDocument

            stored = self.conversations.find_one_and_update(
                {"sender_id": int(sender_id)},
                {"$set": {
                    "sender_id": str(sender_id)
                }},
                return_document=ReturnDocument.AFTER,
            )

        if stored is not None:
            if self.domain:
                return DialogueStateTracker.from_dict(sender_id,
                                                      stored.get("events"),
                                                      self.domain.slots)
            else:
                logger.warning("Can't recreate tracker from mongo storage "
                               "because no domain is set. Returning `None` "
                               "instead.")
                return None
        else:
            return None
コード例 #7
0
ファイル: multi_nlu_client.py プロジェクト: samlet/saai
async def nlu_parse(url, message):
    tracker = DialogueStateTracker.from_dict("1", [],
                                             [Slot("requested_language")])
    # we'll expect this value 'en' to be part of the result from the interpreter
    tracker._set_slot("requested_language", "en")
    inte = RasaNLUHttpInterpreter(EndpointConfig(url))
    result = await inte.parse(message, tracker=tracker)
    return result
コード例 #8
0
ファイル: arcus_tracker_store.py プロジェクト: eternius/asm
 def retrieve(self, sender_id: Text) -> Optional[DialogueStateTracker]:
     tracker = asyncio.get_event_loop().run_until_complete(
         self.asm.memory.get("rasa_tracker", sender_id))
     if tracker:
         _LOGGER.debug("Recreating tracker for id '{}'".format(sender_id))
         if isinstance(tracker, dict):
             return DialogueStateTracker.from_dict(sender_id,
                                                   tracker['events'],
                                                   self.domain.slots)
         else:
             return DialogueStateTracker.from_dict(
                 sender_id,
                 tracker.getStore()['events'], self.domain.slots)
     else:
         _LOGGER.debug(
             "Creating a new tracker for id '{}'.".format(sender_id))
         return None
コード例 #9
0
def test_tracker_without_slots(key, value, caplog):
    event = SlotSet(key, value)
    tracker = DialogueStateTracker.from_dict("any", [])
    assert key in tracker.slots
    with caplog.at_level(logging.INFO):
        event.apply_to(tracker)
        v = tracker.get_slot(key)
        assert v == value
    assert len(caplog.records) == 0
コード例 #10
0
def load_tracker_from_json(tracker_dump: Text,
                           domain: Domain) -> DialogueStateTracker:
    """Read the json dump from the file and instantiate a tracker it."""

    tracker_json = json.loads(rasa.utils.io.read_file(tracker_dump))
    sender_id = tracker_json.get("sender_id", UserMessage.DEFAULT_SENDER_ID)
    return DialogueStateTracker.from_dict(sender_id,
                                          tracker_json.get("events", []),
                                          domain.slots)
コード例 #11
0
ファイル: botfront.py プロジェクト: botfront/rasa-addons
 def _convert_tracker(self, sender_id, tracker):
     if self.domain:
         return DialogueStateTracker.from_dict(sender_id, tracker["events"],
                                               self.domain.slots)
     else:
         logger.warning("Can't recreate tracker from mongo storage "
                        "because no domain is set. Returning `None` "
                        "instead.")
         return None
コード例 #12
0
def test_current_state_no_events(default_agent):
    tracker_dump = "data/test_trackers/tracker_moodbot.json"
    tracker_json = json.loads(rasa.utils.io.read_file(tracker_dump))

    tracker = DialogueStateTracker.from_dict(tracker_json.get("sender_id"),
                                             tracker_json.get("events", []),
                                             default_agent.domain.slots)

    state = tracker.current_state(EventVerbosity.NONE)
    assert state.get("events") is None
コード例 #13
0
    async def tracker_predict(request: Request):
        """ Given a list of events, predicts the next action"""
        validate_request_body(
            request,
            "No events defined in request_body. Add events to request body in order to "
            "predict the next action.",
        )

        sender_id = UserMessage.DEFAULT_SENDER_ID
        request_id = UserMessage.DEFAULT_REQUEST_ID
        user_id = UserMessage.DEFAULT_USER_ID
        verbosity = event_verbosity_parameter(request,
                                              EventVerbosity.AFTER_RESTART)
        request_params = request.json

        try:
            tracker = DialogueStateTracker.from_dict(sender_id, request_id,
                                                     user_id, request_params,
                                                     app.agent.domain.slots)
        except Exception as e:
            logger.debug(traceback.format_exc())
            raise ErrorResponse(
                400,
                "BadRequest",
                "Supplied events are not valid. {}".format(e),
                {
                    "parameter": "",
                    "in": "body"
                },
            )

        try:
            policy_ensemble = app.agent.policy_ensemble
            probabilities, policy = policy_ensemble.probabilities_using_best_policy(
                tracker, app.agent.domain)

            scores = [{
                "action": a,
                "score": p
            } for a, p in zip(app.agent.domain.action_names, probabilities)]

            return response.json({
                "scores": scores,
                "policy": policy,
                "tracker": tracker.current_state(verbosity),
            })
        except Exception as e:
            logger.debug(traceback.format_exc())
            raise ErrorResponse(
                500,
                "PredictionError",
                "An unexpected error occurred. Error: {}".format(e),
            )
コード例 #14
0
ファイル: server.py プロジェクト: kushal1212/Demo_Bot
    async def replace_events(request: Request, sender_id: Text):
        """Use a list of events to set a conversations tracker to a state."""

        request_params = request.json
        verbosity = event_verbosity_parameter(request,
                                              EventVerbosity.AFTER_RESTART)

        tracker = DialogueStateTracker.from_dict(sender_id, request_params,
                                                 app.agent.domain.slots)
        # will override an existing tracker with the same id!
        app.agent.tracker_store.save(tracker)
        return response.json(tracker.current_state(verbosity))
コード例 #15
0
async def generate_response(nlg_call, domain):
    """Mock response generator.

    Generates the responses from the bot's domain file.
    """
    kwargs = nlg_call.get("arguments", {})
    template = nlg_call.get("template")
    sender_id = nlg_call.get("tracker", {}).get("sender_id")
    events = nlg_call.get("tracker", {}).get("events")
    tracker = DialogueStateTracker.from_dict(sender_id, events, domain.slots)
    channel_name = nlg_call.get("channel")

    return await TemplatedNaturalLanguageGenerator(domain.templates).generate(
        template, tracker, channel_name, **kwargs)
コード例 #16
0
ファイル: test_trackers.py プロジェクト: yungliu/rasa_nlu
def test_current_state_after_restart(default_agent):
    tracker_dump = "data/test_trackers/tracker_moodbot.json"
    tracker_json = json.loads(utils.read_file(tracker_dump))

    tracker_json["events"].insert(3, {"event": "restart"})

    tracker = DialogueStateTracker.from_dict(tracker_json.get("sender_id"),
                                             tracker_json.get("events", []),
                                             default_agent.domain.slots)

    events_after_restart = [e.as_dict() for e in list(tracker.events)[4:]]

    state = tracker.current_state(EventVerbosity.AFTER_RESTART)
    assert state.get("events") == events_after_restart
コード例 #17
0
def new_form_and_tracker(form_spec, requested_slot, additional_slots=[]):
    form = ActionBotfrontForm(form_spec.get("form_name"))
    tracker = DialogueStateTracker.from_dict(
        "default",
        [],
        [
            Slot(name=requested_slot),
            *[Slot(name=name) for name in additional_slots],
            Slot(name="requested_slot", initial_value=requested_slot),
            Slot(name="bf_forms", initial_value=[form_spec]),
        ],
    )
    form.form_spec = form_spec  # load spec manually
    return form, tracker
コード例 #18
0
def test_current_state_all_events(default_agent):
    tracker_dump = "data/test_trackers/tracker_moodbot.json"
    tracker_json = json.loads(rasa.utils.io.read_file(tracker_dump))

    tracker_json["events"].insert(3, {"event": "restart"})

    tracker = DialogueStateTracker.from_dict(tracker_json.get("sender_id"),
                                             tracker_json.get("events", []),
                                             default_agent.domain.slots)

    evts = [e.as_dict() for e in tracker.events]

    state = tracker.current_state(EventVerbosity.ALL)
    assert state.get("events") == evts
コード例 #19
0
    def retrieve(self, sender_id: Text) -> Optional[DialogueStateTracker]:
        """Create a tracker from all previously stored events."""

        # Retrieve dialogues for a sender_id in reverse chronological order based on the session_date sort key
        dialogues = self.db.query(
            KeyConditionExpression=Key("sender_id").eq(sender_id),
            Limit=1,
            ScanIndexForward=False,
        )["Items"]
        if dialogues:
            return DialogueStateTracker.from_dict(sender_id,
                                                  dialogues[0].get("events"),
                                                  self.domain.slots)
        else:
            return None
コード例 #20
0
ファイル: tracker_store.py プロジェクト: yalunar/rasa
    def retrieve(self, sender_id: Text) -> DialogueStateTracker:
        """Create a tracker from all previously stored events."""

        query = self.session.query(self.SQLEvent)
        result = query.filter_by(sender_id=sender_id).all()
        events = [json.loads(event.data) for event in result]

        if self.domain and len(events) > 0:
            logger.debug(
                "Recreating tracker from sender id '{}'".format(sender_id))

            return DialogueStateTracker.from_dict(sender_id, events,
                                                  self.domain.slots)
        else:
            logger.debug("Can't retrieve tracker matching"
                         "sender id '{}' from SQL storage.  "
                         "Returning `None` instead.".format(sender_id))
コード例 #21
0
ファイル: test_processor.py プロジェクト: sysang/rasa
async def test_parsing_with_tracker():
    tracker = DialogueStateTracker.from_dict("1", [], [Slot("requested_language")])

    # we'll expect this value 'en' to be part of the result from the interpreter
    tracker._set_slot("requested_language", "en")

    endpoint = EndpointConfig("https://interpreter.com")
    with aioresponses() as mocked:
        mocked.post("https://interpreter.com/parse", repeat=True, status=200)

        # mock the parse function with the one defined for this test
        with patch.object(RasaNLUHttpInterpreter, "parse", mocked_parse):
            interpreter = RasaNLUHttpInterpreter(endpoint_config=endpoint)
            agent = Agent(None, None, interpreter)
            result = await agent.parse_message_using_nlu_interpreter("lunch?", tracker)

            assert result["requested_language"] == "en"
コード例 #22
0
def test_current_state_applied_events(default_agent):
    tracker_dump = "data/test_trackers/tracker_moodbot.json"
    tracker_json = json.loads(rasa.utils.io.read_file(tracker_dump))

    # add some events that result in other events not being applied anymore
    tracker_json["events"].insert(1, {"event": "restart"})
    tracker_json["events"].insert(7, {"event": "rewind"})
    tracker_json["events"].insert(8, {"event": "undo"})

    tracker = DialogueStateTracker.from_dict(tracker_json.get("sender_id"),
                                             tracker_json.get("events", []),
                                             default_agent.domain.slots)

    evts = [e.as_dict() for e in tracker.events]
    applied_events = [evts[2], evts[9]]

    state = tracker.current_state(EventVerbosity.APPLIED)
    assert state.get("events") == applied_events
コード例 #23
0
ファイル: test_trackers.py プロジェクト: pranavdurai10/rasa
def test_session_started_not_part_of_applied_events(default_agent: Agent):
    # take tracker dump and insert a SessionStarted event sequence
    tracker_dump = "data/test_trackers/tracker_moodbot.json"
    tracker_json = json.loads(rasa.shared.utils.io.read_file(tracker_dump))
    tracker_json["events"].insert(
        4, {"event": ActionExecuted.type_name, "name": ACTION_SESSION_START_NAME}
    )
    tracker_json["events"].insert(5, {"event": SessionStarted.type_name})

    # initialise a tracker from this list of events
    tracker = DialogueStateTracker.from_dict(
        tracker_json.get("sender_id"),
        tracker_json.get("events", []),
        default_agent.domain.slots,
    )

    # the SessionStart event was at index 5, the tracker's `applied_events()` should
    # be the same as the list of events from index 6 onwards
    assert tracker.applied_events() == list(tracker.events)[6:]
コード例 #24
0
    def retrieve(self, sender_id: Text) -> Optional[DialogueStateTracker]:
        """Create a tracker from all previously stored events."""
        # Retrieve dialogues for a sender_id in reverse-chronological order based on
        # the session_date sort key
        dialogues = self.db.query(
            KeyConditionExpression=Key("sender_id").eq(sender_id),
            Limit=1,
            ScanIndexForward=False,
        )["Items"]

        if not dialogues:
            return None

        events = dialogues[0].get("events", [])

        # `float`s are stored as `Decimal` objects - we need to convert them back
        events_with_floats = core_utils.replace_decimals_with_floats(events)

        return DialogueStateTracker.from_dict(sender_id, events_with_floats,
                                              self.domain.slots)
コード例 #25
0
    def retrieve(self, sender_id: Text) -> Optional[DialogueStateTracker]:
        """Create a tracker from all previously stored events."""
        # 基于数据库中记录的events,重放出一个tracker
        query = self.session.query(self.SQLEvent)
        result = query.filter_by(sender_id=sender_id).all()
        events = [json.loads(event.data) for event in result]

        if self.domain and len(events) > 0:
            # store定义了domain,并且存在至少一条event
            logger.debug(
                "Recreating tracker from sender id '{}'".format(sender_id))

            return DialogueStateTracker.from_dict(
                sender_id, events, self.domain.slots)  # 基于事件创建tracker
        else:
            # 要么store没定义domain, 要么对应sender id 没有事件
            logger.debug("Can't retrieve tracker matching"
                         "sender id '{}' from SQL storage.  "
                         "Returning `None` instead.".format(sender_id))
            return None
コード例 #26
0
ファイル: tracker_store.py プロジェクト: zhongerqiandan/rasa
    def retrieve(self, sender_id: Text) -> Optional[DialogueStateTracker]:
        """Create a tracker from all previously stored events."""

        import sqlalchemy as sa
        from rasa.core.events import SessionStarted

        with self.session_scope() as session:
            # Subquery to find the timestamp of the latest `SessionStarted` event
            session_start_sub_query = (session.query(
                sa.func.max(
                    self.SQLEvent.timestamp).label("session_start")).filter(
                        self.SQLEvent.sender_id == sender_id,
                        self.SQLEvent.type_name == SessionStarted.type_name,
                    ).subquery())

            results = (
                session.query(self.SQLEvent).filter(
                    self.SQLEvent.sender_id == sender_id,
                    # Find events after the latest `SessionStarted` event or return all
                    # events
                    sa.or_(
                        self.SQLEvent.timestamp >=
                        session_start_sub_query.c.session_start,
                        session_start_sub_query.c.session_start.is_(None),
                    ),
                ).order_by(self.SQLEvent.timestamp).all())

            events = [json.loads(event.data) for event in results]

            if self.domain and len(events) > 0:
                logger.debug(
                    f"Recreating tracker from sender id '{sender_id}'")
                return DialogueStateTracker.from_dict(sender_id, events,
                                                      self.domain.slots)
            else:
                logger.debug(f"Can't retrieve tracker matching "
                             f"sender id '{sender_id}' from SQL storage. "
                             f"Returning `None` instead.")
                return None
コード例 #27
0
    async def retrieve_response_for_request(request: Request):
        message = request.args.get('say')
        sender_id = request.args.get('sender_id')
        print("printing sender id ", sender_id)
        print("printing message ", message)
        output_response = await app.agent.handle_text(message)
        print("printing output response for smalltalk", output_response)
        if output_response == []:
            final_output = {
                "id": sender_id,
                "messages":
                "Sorry, can you rephrase your question and ask again?"
            }
            return response.json(final_output)
        else:
            final_output = {
                "id": sender_id,
                "messages": output_response[0]['text']
            }
            print("printing final output ", final_output)
            return response.json(final_output)
            #return response.text("Hi I am doing good. How about you? ")

        try:
            async with app.agent.lock_store.lock(conversation_id):
                tracker = DialogueStateTracker.from_dict(
                    conversation_id, request.json, app.agent.domain.slots)

                # will override an existing tracker with the same id!
                app.agent.tracker_store.save(tracker)

            return response.json(tracker.current_state(verbosity))
        except Exception as e:
            logger.debug(traceback.format_exc())
            raise ErrorResponse(
                500,
                "ConversationError",
                "An unexpected error occurred. Error: {}".format(e),
            )
コード例 #28
0
ファイル: server.py プロジェクト: kushal1212/Demo_Bot
    async def tracker_predict(request: Request):
        """ Given a list of events, predicts the next action"""

        sender_id = UserMessage.DEFAULT_SENDER_ID
        request_params = request.json
        verbosity = event_verbosity_parameter(request,
                                              EventVerbosity.AFTER_RESTART)

        try:
            tracker = DialogueStateTracker.from_dict(sender_id, request_params,
                                                     app.agent.domain.slots)
        except Exception as e:
            raise ErrorResponse(
                400,
                "InvalidParameter",
                "Supplied events are not valid. {}".format(e),
                {
                    "parameter": "",
                    "in": "body"
                },
            )

        policy_ensemble = app.agent.policy_ensemble
        probabilities, policy = policy_ensemble.probabilities_using_best_policy(
            tracker, app.agent.domain)

        scores = [{
            "action": a,
            "score": p
        } for a, p in zip(app.agent.domain.action_names, probabilities)]

        return response.json({
            "scores": scores,
            "policy": policy,
            "tracker": tracker.current_state(verbosity),
        })
コード例 #29
0
    def retrieve(self, sender_id: Text) -> Optional[DialogueStateTracker]:
        """Create a tracker from all previously stored events."""

        import sqlalchemy as sa
        from rasa.core.events import SessionStarted

        with self.session_scope() as session:

            serialised_events = self._event_query(session, sender_id).all()

            events = [json.loads(event.data) for event in serialised_events]

            if self.domain and len(events) > 0:
                logger.debug(f"Recreating tracker from sender id '{sender_id}'")
                return DialogueStateTracker.from_dict(
                    sender_id, events, self.domain.slots
                )
            else:
                logger.debug(
                    f"Can't retrieve tracker matching "
                    f"sender id '{sender_id}' from SQL storage. "
                    f"Returning `None` instead."
                )
                return None
コード例 #30
0
def _chat_history_table(events: List[Dict[Text, Any]]) -> Text:
    """Create a table containing bot and user messages.

    Also includes additional information, like any events and
    prediction probabilities."""

    def wrap(txt: Text, max_width: int) -> Text:
        return "\n".join(textwrap.wrap(txt, max_width, replace_whitespace=False))

    def colored(txt: Text, color: Text) -> Text:
        return "{" + color + "}" + txt + "{/" + color + "}"

    def format_user_msg(user_event: UserUttered, max_width: int) -> Text:
        intent = user_event.intent or {}
        intent_name = intent.get("name", "")
        _confidence = intent.get("confidence", 1.0)
        _md = _as_md_message(user_event.parse_data)

        _lines = [
            colored(wrap(_md, max_width), "hired"),
            "intent: {} {:03.2f}".format(intent_name, _confidence),
        ]
        return "\n".join(_lines)

    def bot_width(_table: AsciiTable) -> int:
        return _table.column_max_width(1)

    def user_width(_table: AsciiTable) -> int:
        return _table.column_max_width(3)

    def add_bot_cell(data, cell):
        data.append([len(data), Color(cell), "", ""])

    def add_user_cell(data, cell):
        data.append([len(data), "", "", Color(cell)])

    # prints the historical interactions between the bot and the user,
    # to help with correctly identifying the action
    table_data = [
        [
            "#  ",
            Color(colored("Bot      ", "autoblue")),
            "  ",
            Color(colored("You       ", "hired")),
        ]
    ]

    table = SingleTable(table_data, "Chat History")

    bot_column = []

    tracker = DialogueStateTracker.from_dict("any", events)
    applied_events = tracker.applied_events()

    for idx, event in enumerate(applied_events):
        if isinstance(event, ActionExecuted):
            bot_column.append(colored(event.action_name, "autocyan"))
            if event.confidence is not None:
                bot_column[-1] += colored(
                    " {:03.2f}".format(event.confidence), "autowhite"
                )

        elif isinstance(event, UserUttered):
            if bot_column:
                text = "\n".join(bot_column)
                add_bot_cell(table_data, text)
                bot_column = []

            msg = format_user_msg(event, user_width(table))
            add_user_cell(table_data, msg)

        elif isinstance(event, BotUttered):
            wrapped = wrap(format_bot_output(event), bot_width(table))
            bot_column.append(colored(wrapped, "autoblue"))

        else:
            if event.as_story_string():
                bot_column.append(wrap(event.as_story_string(), bot_width(table)))

    if bot_column:
        text = "\n".join(bot_column)
        add_bot_cell(table_data, text)

    table.inner_heading_row_border = False
    table.inner_row_border = True
    table.inner_column_border = False
    table.outer_border = False
    table.justify_columns = {0: "left", 1: "left", 2: "center", 3: "right"}

    return table.table