示例#1
0
    def evalute_normal_single_worker_n_job(self,
                                           env,
                                           job=None):  # worker = None,
        # return score, violated_rules (negative values)
        # return self.weight * 1
        # Now check if this new job can fit into existing

        score = 1
        overall_message = "Job ({}) requires skill ({}), workers: {}. ".format(
            job.job_code, job.requested_skills, job.scheduled_worker_codes)
        metrics_detail = {"status_code": "OK"}

        for worker_code in job.scheduled_worker_codes:
            worker = env.workers_dict[worker_code]

            # TODO check skill_keys match as set, which should be faster.
            worker_skill_key_set = set(worker.skills.keys())

            for skill_key in job.requested_skills:
                if skill_key not in worker_skill_key_set:
                    overall_message += "Skill key({}) is not found in worker!".format(
                        skill_key)
                    score = -1
                    metrics_detail = {"missing_key": skill_key}
                    break
                for skill in job.requested_skills[skill_key]:
                    if skill not in worker.skills[skill_key]:
                        overall_message += "({}={}) is not found in worker {}!".format(
                            skill_key, skill, worker_code)
                        score = -1
                        metrics_detail = {"missing_value": {skill_key: skill}}
                        break

        score_res = ActionEvaluationScore(
            score=score,
            score_type=self.title,
            message=overall_message,
            metrics_detail=metrics_detail,
        )
        return score_res
示例#2
0
    def evalute_normal_single_worker_n_job(self,
                                           env,
                                           job=None):  # worker = None,
        # return score, violated_rules (negative values)
        # return self.weight * 1
        # Now check if this new job can fit into existing

        score = 1
        overall_message = "Job ({}) requires items ({}) on workers {}. \n".format(
            job.job_code, job.requested_items, job.scheduled_worker_codes)
        metrics_detail = {"status_code": "OK"}
        total_loaded_items = {k: 0 for k in job.requested_items.keys()}

        total_requested_items = dict(job.requested_items)
        inspected_jobs = set()

        all_slots = []
        # First I aggregate all items from all workers
        for worker_code in job.scheduled_worker_codes:
            worker = env.workers_dict[worker_code]
            overlapped_slots = env.slot_server.get_overlapped_slots(
                worker_id=worker_code,
                start_minutes=job.scheduled_start_minutes,
                end_minutes=job.scheduled_start_minutes +
                job.scheduled_duration_minutes)
            if len(overlapped_slots) < 1:
                overall_message += " but no slot was found!"
                score = -1
                metrics_detail = {}
                break
            slot = overlapped_slots[0]

            # It should match only one, but if more, I take only first one.
            for jc in slot.assigned_job_codes:
                if jc in inspected_jobs:
                    # This should be summerized only once for more workers.
                    # Skip from this loop
                    continue
                else:
                    inspected_jobs.add(jc)
                    # move on to check
                r_items = env.jobs_dict[jc].requested_items
                for ik in r_items.keys():
                    if ik not in total_requested_items.keys():
                        total_requested_items[ik] = r_items[ik]
                    else:
                        total_requested_items[ik] += r_items[ik]

                    if ik not in total_loaded_items.keys():
                        total_requested_items[ik] = 0

            # all_slots[0] is the first worker slot, i.e. primary
            all_slots.append(slot)
            # TODO check item_keys match as set, which should be faster.
            for k in slot.loaded_items.keys():
                if k in total_loaded_items.keys():
                    total_loaded_items[k] += slot.loaded_items[k]

        # Second I verify that aggregated items list can fulfil job requirement
        # TODO, duan, 2021-10-23 22:13:35. I should loop through all depots in future.
        depot_key = list(env.depots_dict.keys())[0]
        inventory_dict = env.kp_data_adapter.get_depot_item_inventory_dict(
            depot_id=env.depots_dict[depot_key]["id"],
            requested_items=list(total_requested_items.keys()))
        # loaded_items_key_set = set(total_loaded_items.keys())
        for item_key in job.requested_items:
            item_qty = total_loaded_items[item_key]
            # if item_key in loaded_items_key_set:
            #     item_qty = total_loaded_items[item_key]

            if job.requested_items[item_key] > item_qty:
                if job.requested_items[item_key] > inventory_dict[item_key]:
                    # This items is also NOT available in depot/warehouse
                    overall_message += ", requested {} > {}; but only {} found on workers {}, and {} found in depot!".format(
                        item_key, job.requested_items[item_key], item_qty,
                        job.scheduled_worker_codes, inventory_dict[item_key])
                    score = -1
                    metrics_detail = {"item": item_key, "status_code": "ERROR"}
                    break
                else:
                    # This items IS available in depot/warehouse.
                    # Then I will add a virtual replenish job and move on.
                    # The replenish job is only added to primary worker as all_slots[0]
                    # replenish_job = env.mutate_create_replenish_job(slot = all_slots[0], total_loaded_items = total_loaded_items)
                    # if replenish_job is None:
                    #     overall_message += ", requested {} > {}; only {} found on workers {}, and failed insert replenish job!".format(
                    #         item_key,
                    #         job.requested_items[item_key],
                    #         item_qty,
                    #         job.scheduled_worker_codes,
                    #     )
                    #     score = -1
                    #     metrics_detail = {"item":  item_key, "status_code":"ERROR" }
                    #     break

                    overall_message += ", requested {} > {}; only {} found on workers {}, and a replenishment job is needed!".format(
                        item_key,
                        job.requested_items[item_key],
                        item_qty,
                        job.scheduled_worker_codes,
                    )
                    score = 0
                    metrics_detail = {
                        "item": item_key,
                        "status_code": "WARNING",
                        # "slot_code":env.slot_server.get_time_slot_key(slot),
                        "slot": slot,
                        "total_requested_items": total_requested_items,
                        "total_loaded_items": total_loaded_items,
                    }
                    # allows to move on

                # Move on to check next item.
                # break

        score_res = ActionEvaluationScore(
            score=score,
            score_type=self.title,
            message=overall_message,
            metrics_detail=metrics_detail,
        )
        return score_res
示例#3
0
    def evalute_normal_single_worker_n_job(self,
                                           env,
                                           job=None):  # worker = None,
        # return score, violated_rules (negative values)
        # return self.weight * 1
        # Now check if this new job can fit into existing

        score = 1
        job_requested_items = job.requested_items
        requested_volume = sum([
            job_requested_items[item_key] * env.items_dict[item_key]["volume"]
            for item_key in job_requested_items.keys()
        ])
        requested_weight = sum([
            job_requested_items[item_key] * env.items_dict[item_key]["weight"]
            for item_key in job_requested_items.keys()
        ])

        overall_message = "Job ({}) produces waste of volume ({}), weight ({}). ".format(
            job.job_code, requested_volume, requested_weight)
        metrics_detail = {"status_code": "OK"}

        if (requested_volume < 0) or (requested_weight < 0):

            worker_code = job.scheduled_worker_codes[0]
            overlapped_slots = env.slot_server.get_overlapped_slots(
                worker_id=worker_code,
                start_minutes=job.scheduled_start_minutes,
                end_minutes=job.scheduled_start_minutes +
                job.scheduled_duration_minutes)

            if len(overlapped_slots) < 1:
                overall_message += " but no slot was found!"
                score = -1
                metrics_detail = {}
            else:
                slot = overlapped_slots[0]

                spare_weight = slot.max_weight - sum([
                    slot.loaded_items[item_key] *
                    env.items_dict[item_key]["weight"]
                    for item_key in slot.loaded_items.keys()
                ])
                spare_volume = slot.max_weight - sum([
                    slot.loaded_items[item_key] *
                    env.items_dict[item_key]["volume"]
                    for item_key in slot.loaded_items.keys()
                ])

                if (spare_weight + requested_weight <
                        0) or (spare_volume + requested_volume < 0):
                    overall_message += ", slot spare weight {} and  spare volume {} are found!".format(
                        spare_weight,
                        spare_volume,
                    )
                    score = -1
                    metrics_detail = {}
        else:
            overall_message = "Job ({}) produces no waste.".format(
                job.job_code)
        score_res = ActionEvaluationScore(
            score=score,
            score_type=self.title,
            message=overall_message,
            metrics_detail=metrics_detail,
        )
        return score_res
示例#4
0
    def evalute_normal_single_worker_n_job(self, env=None, job=None):  # worker = None,
        overall_message = ""
        score = 1
        metrics_detail = {"status_code": "OK"}

        for worker_code in job.scheduled_worker_codes:
            worker = env.workers_dict[worker_code]

            day_seq = int(job.scheduled_start_minutes / 1440)
            weekday_i = env.env_encode_day_seq_to_weekday(day_seq)
            slot_in_day_count = len(worker.weekly_working_slots[weekday_i])
            slot_intersected=False
            slot_covered = False
            for slot_in_day_i, slot_in_day in enumerate(worker.weekly_working_slots[weekday_i]):
                working_slot_in_the_day = [
                    slot_in_day[0] + (24 * 60 * day_seq),
                    slot_in_day[1] + (24 * 60 * day_seq),
                ]
                clipped_slot = date_util.clip_time_period(
                    p1=working_slot_in_the_day,
                    p2=[
                        job.scheduled_start_minutes,
                        job.scheduled_start_minutes + job.scheduled_duration_minutes,
                    ],
                )
                if len(clipped_slot) > 1:
                    slot_intersected = True
                    if (clipped_slot[0] == job.scheduled_start_minutes) & (
                        clipped_slot[1] == job.scheduled_start_minutes + job.scheduled_duration_minutes
                    ):
                        overall_message = self.success_message_template.format(
                            date_util.minutes_to_time_string(job.scheduled_start_minutes),
                            date_util.minutes_to_time_string(
                                job.scheduled_start_minutes + job.scheduled_duration_minutes
                            ),
                            job.scheduled_start_minutes,
                            job.scheduled_start_minutes + job.scheduled_duration_minutes,
                        )
                        # move on to next worker
                        slot_covered = True
                        break
            if slot_covered:
                continue
            available_overtime = env.get_worker_available_overtime_minutes(
                worker_code=worker_code, day_seq=day_seq
            )
            # if self.config["allow_overtime"]:
            if slot_intersected & (available_overtime > 0):
                start_with_overtime = working_slot_in_the_day[0]
                if slot_in_day_i ==0:
                    start_with_overtime -= available_overtime

                end_with_overtime = working_slot_in_the_day[1]
                if slot_in_day_i == slot_in_day_count - 1:
                    end_with_overtime += available_overtime


                if (
                    ( 
                        start_with_overtime < job.scheduled_start_minutes
                    ) & (
                        end_with_overtime > job.scheduled_start_minutes + job.scheduled_duration_minutes
                    ) & (
                        working_slot_in_the_day[1]
                        - working_slot_in_the_day[0]
                        + available_overtime
                        > job.scheduled_duration_minutes
                    )
                ):
                    score = 0
                    overall_message = self.overtime_allowed_message_template.format(
                        date_util.minutes_to_time_string(job.scheduled_start_minutes),
                        date_util.minutes_to_time_string(
                            job.scheduled_start_minutes + job.scheduled_duration_minutes
                        ),
                        job.scheduled_start_minutes,
                        job.scheduled_start_minutes + job.scheduled_duration_minutes,
                        available_overtime,
                    )
                    # move on to next worker
                    print(overall_message)
                    continue
            
            # If the time slot were ok (i.e. included in any slot), it should have been skipped by continue  
            # If the start time does not fully fall in one working slot for any worker, reject it instantly.
            score = -1
            overall_message = self.message_template.format(
                date_util.minutes_to_time_string(job.scheduled_start_minutes),
                date_util.minutes_to_time_string(
                    job.scheduled_start_minutes + job.scheduled_duration_minutes
                ),
                job.scheduled_start_minutes,
                job.scheduled_start_minutes + job.scheduled_duration_minutes,
                available_overtime,
            )
            return ActionEvaluationScore(
                score=score,
                score_type=self.title,
                message=overall_message,
                metrics_detail={"status_code": "ERROR"},
            )

        return ActionEvaluationScore(
            score=score,
            score_type=self.title,
            message=overall_message,
            metrics_detail=metrics_detail,
        )
示例#5
0
    def evalute_normal_single_worker_n_job(self, env=None, job=None):  # worker = None,
        # action_dict = env.decode_action_into_dict(action)
        # if (action_dict['scheduled_start_minutes'] > 14*60 ) | (action_dict['scheduled_start_minutes'] + action_dict['scheduled_duration_minutes']< 12*60  ):
        # scheduled_start_minutes_local = job.scheduled_start_minutes % (24 * 60) 
        score = 1
        metrics_detail = {"status_code": "OK"}

        action_day = int(job.scheduled_start_minutes / 24 / 60)

        job_lunch_start = action_day * 24 * 60 + (self.config["lunch_start_hour"] * 60)
        job_lunch_end = action_day * 24 * 60 + (self.config["lunch_end_hour"] * 60)
        overlap_lunch = date_util.clip_time_period(
            [job_lunch_start, job_lunch_end],
            [
                job.scheduled_start_minutes,
                job.scheduled_start_minutes + job.scheduled_duration_minutes,
            ],
        )
        if len(overlap_lunch) < 1:
            overall_message = "The Job is not at lunch time"

            score_res = ActionEvaluationScore(
                score=score,
                score_type=self.title,
                message=overall_message,
                metrics_detail=metrics_detail,
            )
            return score_res

        overall_message = f"Lunch break > {self.config['lunch_break_minutes']} minutes"
        # scheduled_start_minutes = job.scheduled_start_minutes
        job_start_minutes = job.scheduled_start_minutes
        job_end_minutes = job.scheduled_start_minutes + job.scheduled_duration_minutes

        # for job_i in range(len(env.workers_dict[worker_code]["assigned_jobs"])):
        # for worker_id in [job.scheduled_worker_codes[0]] + job["scheduled_secondary_worker_ids"]:
        for worker_id in job.scheduled_worker_codes:

            total_avail_lunch_break = job_lunch_end - job_lunch_start
            overlapped_slots = env.slot_server.get_overlapped_slots(
                worker_id=worker_id, start_minutes=job_start_minutes, end_minutes=job_end_minutes
            )
            # all_jobs = reduce(lambda x, y: x.assigned_job_codes + y.assigned_job_codes, overlapped_slots)

            for a_slot in overlapped_slots:

                (
                    prev_travel,
                    next_travel,
                    inside_travel,
                ) = env.get_travel_time_jobs_in_slot(a_slot, a_slot.assigned_job_codes)
                all_prev_travel_minutes = [prev_travel] + inside_travel  # + inside_travel
                if a_slot.slot_type == TimeSlotType.JOB_FIXED:
                    total_avail_lunch_break -= (
                        prev_travel +
                        # env.jobs_dict[job.job_code].scheduled_duration_minutes
                        job.scheduled_duration_minutes
                    )
                elif a_slot.slot_type == TimeSlotType.FLOATING:
                    for job_seq, job_code in enumerate(a_slot.assigned_job_codes):
                        a_job = env.jobs_dict[job_code]
                        a_job_period = [
                            a_job.scheduled_start_minutes - all_prev_travel_minutes[job_seq],
                            a_job.scheduled_start_minutes + a_job.scheduled_duration_minutes,
                        ]
                        a_job_period_lunch_overlap = date_util.clip_time_period(
                            [job_lunch_start, job_lunch_end], a_job_period
                        )
                        if len(a_job_period_lunch_overlap) > 1:
                            total_avail_lunch_break -= (
                                a_job_period_lunch_overlap[1] - a_job_period_lunch_overlap[0]
                            )
                else:
                    # raise LookupError("unknown slot type - E?")
                    pass  # lunch break in dairy events/absence

            if total_avail_lunch_break < self.config["lunch_break_minutes"]:
                # For now, lunch break does not enforce to -1, lowest is 0, as warning
                score = 0
                overall_message = f"total_avail_lunch_break = {total_avail_lunch_break}, which is less than MINIMUM({self.config['lunch_break_minutes']}) for worker {worker_id}"

        score_res = ActionEvaluationScore(
            score=score,
            score_type=self.title,
            message=overall_message,
            metrics_detail=metrics_detail,
        )
        return score_res
示例#6
0
    def search_action_dict_on_workers(
        self,
        a_worker_code_list: List[str],
        curr_job: BaseJob,
        max_number_of_matching: int = 9,
        attempt_unplan_jobs=False,
        allow_overtime=False,
    ) -> List[RecommendedAction]:
        # if "job_gps" not in curr_job.keys():
        #    curr_job["job_gps"] = ([curr_job.location[0], curr_job.location[1]],)
        if curr_job.job_code in APPOINTMENT_DEBUG_LIST:
            log.info(
                f"appt={curr_job.job_code}, worker_codes:{a_worker_code_list} started searching...")
        if (curr_job.requested_duration_minutes is None) or (curr_job.requested_duration_minutes < 1):
            log.error(
                f"appt={curr_job.job_code}, requested_duration_minutes = {curr_job.requested_duration_minutes}, no recommendation is possible, quitting...")
            return []
        scheduled_duration_minutes = (self.env.get_encode_shared_duration_by_planning_efficiency_factor(
            requested_duration_minutes=curr_job.requested_duration_minutes,
            nbr_workers=len(a_worker_code_list),
        ))
        # if len(a_worker_code_list) > 1:
        # else:
        #     scheduled_duration_minutes = curr_job.requested_duration_minutes

        curr_job.scheduled_duration_minutes = scheduled_duration_minutes
        # self.env.jobs_dict[
        #     curr_job.job_code
        # ].scheduled_duration_minutes = scheduled_duration_minutes

        result_slot = []
        time_slot_list = []

        original_travel_minutes_difference = 0
        if curr_job.planning_status != JobPlanningStatus.UNPLANNED:
            for worker_code in curr_job.scheduled_worker_codes:
                a_slot_group = self.env.slot_server.get_overlapped_slots(
                    worker_id=worker_code,
                    start_minutes=curr_job.scheduled_start_minutes,
                    end_minutes=curr_job.scheduled_start_minutes + curr_job.scheduled_duration_minutes,
                )
                for a_slot in a_slot_group:
                    try:
                        original_travel_minutes_difference += (self.calc_travel_minutes_difference_for_1_job(
                            env=self.env, a_slot=a_slot, curr_job=curr_job))
                    except ValueError as err:
                        log.debug(
                            f"JOB:{curr_job.job_code}:ERROR:{err}, This job has lost the worker time slot, For now, no more recomemndations. and then I set original_travel_minutes_difference = 0")
                        original_travel_minutes_difference = 0
                        # For now, no more recomemndations , TODO @duan
                        # return []

        for curr_worker_code in a_worker_code_list:
            if curr_worker_code not in self.env.workers_dict.keys():
                log.error(
                    f"WORKER:{curr_worker_code}: Worker is requested to be searched but not found in env.")
                return []
            log.debug(
                f"Querying slots, worker_id={curr_worker_code}, start_minutes={curr_job.requested_start_min_minutes}, end_minutes={curr_job.requested_start_max_minutes}")
            curr_overlapped_slot_list = self.env.slot_server.get_overlapped_slots(
                worker_id=curr_worker_code,
                start_minutes=curr_job.requested_start_min_minutes,
                end_minutes=curr_job.requested_start_max_minutes,
            )

            curr_free_slot_list = []
            for slot in curr_overlapped_slot_list:

                log.debug(f"Checking slot: {self.env.slot_server.get_time_slot_key(slot)}")
                if slot.slot_type == TimeSlotType.FLOATING:
                    if allow_overtime:
                        # If attempt_unplan_jobs is set true, this is already secondary round.
                        # I will also consider the overtime minutes for each worker
                        #
                        new_overtime_minutes = self.env.get_worker_available_overtime_minutes(
                            slot.worker_id, day_seq=int(slot.start_minutes / 1440))
                        if slot.prev_slot_code is None:
                            slot.start_overtime_minutes += new_overtime_minutes
                        if slot.next_slot_code is None:
                            slot.end_overtime_minutes += new_overtime_minutes

                    if (slot.end_minutes - slot.start_minutes + slot.start_overtime_minutes + slot.end_overtime_minutes) > scheduled_duration_minutes:
                        # This may screen out some apparently no fitting slots, without considering travel time.

                        curr_free_slot_list.append(slot)
                        log.debug(
                            f"worker_codes:{a_worker_code_list}:worker:{curr_worker_code}: identified one free slot ({slot.start_minutes}->{slot.end_minutes}).")

            if len(curr_free_slot_list) < 1:
                log.debug(
                    f"appt={curr_job.job_code}, worker_codes:{a_worker_code_list}:worker:{curr_worker_code}: Worker has no free slots left by tolerance ({curr_job.requested_start_min_minutes}->{curr_job.requested_start_max_minutes}) ({self.env.env_decode_from_minutes_to_datetime(curr_job.requested_start_min_minutes)}->{self.env.env_decode_from_minutes_to_datetime(curr_job.requested_start_max_minutes)})."
                )
                return []
            # if curr_job.job_code in APPOINTMENT_DEBUG_LIST:
            # log.debug(
            #     "All free slots identified: "
            #     + str(
            #         [
            #             f"worker_id = {w.worker_id}, start_minutes= {w.start_minutes}, end_minutes= {w.end_minutes}, assigned_job_codes= {w.assigned_job_codes}"
            #             for w in curr_free_slot_list
            #         ]
            #     )
            # )
            time_slot_list.append(sorted(
                curr_free_slot_list,
                key=lambda item: item.start_minutes,
            ))

        if len(time_slot_list) < 1:
            log.warn("should not happend: len(time_slot_list) < 1")
            return []

        # This appends the customer avavailable slots to be the last timeslot list to screen against all work's timeslots.
        # I assume that customer does not like current time slot and remove it from available slot,
        # TODO, @duan, 2020-12-17 19:23:53 Why available_slots is on locaiton? different jobs may differ.

        orig_avail = copy.deepcopy(curr_job.available_slots)
        if len(orig_avail) < 1:
            log.info(
                f"appt={curr_job.job_code}, job_available_slots:{orig_avail}: No available slots per customer availability.")
            return []
        else:
            # if curr_job.job_code in APPOINTMENT_DEBUG_LIST:
            log.debug(
                f"appt={curr_job.job_code}, len(orig_avail) ={len(orig_avail)}: Found available slots per customer availability.")

        # This fake_appt_time_slot will used to host the slot intersection result, especially after customer availability
        fake_appt_time_slot = copy.deepcopy(orig_avail[0])
        fake_appt_time_slot.assigned_job_codes = [curr_job.job_code]

        # This is to exclude those available slots that overlap with current scheduling time.
        # Make sure that customer do not get same time slots (meaningless, from other technicians) as current one.
        net_cust_avail_slots = []
        slot_to_exclude = (
            curr_job.scheduled_start_minutes - 60,
            curr_job.scheduled_start_minutes + 90,
        )
        for ti in range(len(orig_avail)):
            slot_ti = (orig_avail[ti].start_minutes, orig_avail[ti].end_minutes)
            clip = date_util.clip_time_period(slot_ti, slot_to_exclude)
            if len(clip) < 1:
                net_cust_avail_slots.append(orig_avail[ti])
            else:
                if clip[0] > orig_avail[ti].start_minutes:
                    a_slot = copy.copy(orig_avail[ti])
                    a_slot.end_minutes = clip[0]
                    net_cust_avail_slots.append(a_slot)
                if clip[1] < orig_avail[ti].end_minutes:
                    a_slot = copy.copy(orig_avail[ti])
                    a_slot.start_minutes = clip[1]
                    net_cust_avail_slots.append(a_slot)

        # To include the customer available time slots in the search
        time_slot_list.append(net_cust_avail_slots)
        # = {net_cust_avail_slots}
        log.debug(f"Final net_cust_avail_slots len={len(net_cust_avail_slots)}")

        available_slot_groups = self.env.intersect_geo_time_slots(
            time_slot_list,
            curr_job,
            duration_minutes=scheduled_duration_minutes,
            max_number_of_matching=max_number_of_matching,
        )

        # if ( free_slot.start_minutes <= start_minutes - travel_minutes ) &  ( end_minutes <= free_slot[1]):
        # if len(available_slot_groups) < 1:
        # no hit in this day_i
        #    continue
        log.debug(
            f"after env.intersect_geo_time_slots, available_slot_groups len={len(available_slot_groups)}.")
        for avail_slot_group in available_slot_groups:
            scoped_slot_list = avail_slot_group[2]
            shared_time_slots_temp = []
            for sc_slot in scoped_slot_list[:-1]:
                try:
                    # TODO, merge this with first filtering. with overtime minutes processing 2021-01-01 10:18:06
                    sc = self.env.slot_server.get_time_slot_key(sc_slot)
                    temp_slot_ = self.env.slot_server.get_slot(
                        redis_handler=self.env.slot_server.r, slot_code=sc)
                    temp_slot_.start_overtime_minutes = sc_slot.start_overtime_minutes
                    temp_slot_.end_overtime_minutes = sc_slot.end_overtime_minutes
                    shared_time_slots_temp.append(temp_slot_)
                except Exception as mse:
                    log.error(f"failed to read slot {str(sc)}, error {str(mse)}")
            if len(shared_time_slots_temp) < 1:
                # After checking redis, there is no valid slot in the list
                log.info(
                    f"appt={curr_job.job_code}, After checking redis, there is no valid slot in the list, No available slots.")
                continue

            shared_time_slots_temp.append(scoped_slot_list[-1])

            shared_time_slots = shared_time_slots_temp.copy()
            # shared_time_slots_optimized may be changed/mutated during optimization process. Deep copy it.
            shared_time_slots_optimized = copy.deepcopy(shared_time_slots)

            for one_working_slot in shared_time_slots_optimized:
                one_working_slot.assigned_job_codes = sorted(
                    one_working_slot.assigned_job_codes,
                    key=lambda x: self.env.jobs_dict[x].scheduled_start_minutes
                )

                one_working_slot.assigned_job_codes.append(curr_job.job_code)

            # TODO, then append other related shared code slots... Right now only this one.

            # This is the last attached fake time slot according to availability search
            # TODO: WHy two availability?
            fake_appt_time_slot.start_minutes = avail_slot_group[0]
            fake_appt_time_slot.end_minutes = avail_slot_group[1]
            shared_time_slots_optimized.append(fake_appt_time_slot)

            if self.use_naive_search_for_speed:
                dispatch_jobs_in_slots_func = self.env.inner_slot_heur.dispatch_jobs_in_slots
            else:
                dispatch_jobs_in_slots_func = self.env.inner_slot_opti.dispatch_jobs_in_slots

            unplanned_job_codes = []
            # res = {"status":OptimizerSolutionStatus.INFEASIBLE}
            res = dispatch_jobs_in_slots_func(shared_time_slots_optimized)
            while attempt_unplan_jobs & (res.status != OptimizerSolutionStatus.SUCCESS):
                is_shrinked = False
                for ts in shared_time_slots_optimized:
                    if len(ts.assigned_job_codes) > 1:
                        j_i = len(ts.assigned_job_codes) - 2
                        while j_i >= 0:
                            job_code_to_unplan = ts.assigned_job_codes[j_i]
                            if self.env.jobs_dict[job_code_to_unplan].priority < curr_job.priority:
                                unplanned_job_codes.append(job_code_to_unplan)
                                ts.assigned_job_codes = ts.assigned_job_codes[0:j_i] + \
                                    ts.assigned_job_codes[j_i + 1:]
                                is_shrinked = True

                                log.info(
                                    f"job={curr_job.job_code}, attempting search after unplanning job {job_code_to_unplan}, to {ts.assigned_job_codes}, priority { self.env.jobs_dict[job_code_to_unplan].priority , curr_job.priority} ...")
                                break
                            j_i -= 1
                    if is_shrinked:
                        # only unplan one job in each attempt
                        break
                if not is_shrinked:
                    # No job can be unplanned.
                    break
                res = dispatch_jobs_in_slots_func(shared_time_slots_optimized)
                if res.status == OptimizerSolutionStatus.SUCCESS:
                    break

            if res.status != OptimizerSolutionStatus.SUCCESS:
                log.info(f"appt={curr_job.job_code}, failed to find solution on slots: " + str(
                    [f"(worker_id = {w.worker_id}, start_datetime= {self.env.env_decode_from_minutes_to_datetime(w.start_minutes)}, end_datetime= {self.env.env_decode_from_minutes_to_datetime(w.end_minutes)}, assigned_job_codes= {w.assigned_job_codes}) " for w in shared_time_slots_optimized]))
                continue
            try:

                new_start_mintues = res.changed_action_dict_by_job_code[curr_job.job_code].scheduled_start_minutes
            except KeyError:
                log.error(
                    f"{curr_job.job_code} is not in changed_action_dict_by_job_code. I should have already excluded it from search.")
                continue

            new_action_dict = ActionDict(
                is_forced_action=False,
                job_code=curr_job.job_code,
                action_type=ActionType.JOB_FIXED,
                scheduled_worker_codes=a_worker_code_list,
                scheduled_start_minutes=new_start_mintues,
                scheduled_duration_minutes=curr_job.scheduled_duration_minutes,
            )
            rule_check_result = self.env._check_action_on_rule_set(
                a_dict=new_action_dict, unplanned_job_codes=unplanned_job_codes)
            # TODO, track Failed number
            if rule_check_result.status_code == ActionScoringResultType.ERROR:
                log.info(
                    f"Failed on rule set check after acquiring recommendation. Skipped. appt = {curr_job.job_code}, workers = {a_worker_code_list}, start = {new_start_mintues}, messages = {['{}---{}---{}'.format(m.score,m.score_type, m.message) for m in rule_check_result.messages]} "
                )

                continue
            # This may not be necessary , because I have executed optimize_slot and the slot contains only possible jobs
            #
            # cut_off_success_flag, message_dict = self.env.slot_server.cut_off_time_slots(
            #     action_dict=new_action_dict, probe_only=True
            # )
            # if not cut_off_success_flag:
            #     log.info(
            #         f"After rule set, failed on probing cutting off slots. appt = {curr_job.job_code}, workers = {a_worker_code_list}, start = {new_start_mintues}, messages = {str(message_dict)} "
            #     )
            #     continue

            log.info(
                f"appt = {curr_job.job_code}, workers = {a_worker_code_list}, start = {new_start_mintues}, The solution passed all rules.")

            #  Now I should trim the last time slot attached as customer availability
            scoped_slot_code_list_no_cust = [self.env.slot_server.get_time_slot_key(
                s) for s in scoped_slot_list[0:len(a_worker_code_list)]]
            # Here i should also assert that len(scoped_slot_code_list) == worker_lenth + 1

            new_travel_minutes_difference = 0
            for a_slot in shared_time_slots:
                (
                    prev_travel,
                    next_travel,
                    inside_travel,
                ) = self.env.get_travel_time_jobs_in_slot(a_slot, a_slot.assigned_job_codes)
                new_travel_minutes_difference -= prev_travel + next_travel + sum(inside_travel)

            for a_slot in shared_time_slots_optimized:
                (
                    prev_travel,
                    next_travel,
                    inside_travel,
                ) = self.env.get_travel_time_jobs_in_slot(a_slot, a_slot.assigned_job_codes)
                new_travel_minutes_difference += prev_travel + next_travel + sum(inside_travel)

            total_score = ((self.env.config["scoring_factor_standard_travel_minutes"] - new_travel_minutes_difference + original_travel_minutes_difference) /
                           self.env.config["scoring_factor_standard_travel_minutes"]) - (len(unplanned_job_codes) * 0.5)

            if allow_overtime:
                total_score -= 2

            travel_message = f"original minutes difference:{original_travel_minutes_difference}, new minutes difference: {new_travel_minutes_difference}"
            metrics_detail = {
                "original_minutes_difference": original_travel_minutes_difference,
                "new_minutes_difference": new_travel_minutes_difference,
            }
            travel_score_obj = ActionEvaluationScore(
                score=total_score,
                score_type="travel_difference",
                message=travel_message,
                metrics_detail=metrics_detail,
            )
            # if curr_job.job_code == "04856415-b6ae-4aeb-9e6d-ff399f00de0d":
            #     log.debug("04856415-b6ae-4aeb-9e6d-ff399f00de0d")

            a_rec = RecommendedAction(
                job_code=curr_job.job_code,
                # action_type=ActionType.JOB_FIXED,
                # JobType = JOB, which can be acquired from self.jobs_dict[job_code]
                scheduled_worker_codes=a_worker_code_list,
                scheduled_start_minutes=new_start_mintues,
                scheduled_duration_minutes=curr_job.scheduled_duration_minutes,
                score=total_score,
                score_detail=[travel_score_obj],
                scoped_slot_code_list=scoped_slot_code_list_no_cust,
                job_plan_in_scoped_slots=res.planned_job_sequence[0:len(a_worker_code_list)],
                unplanned_job_codes=unplanned_job_codes,
            )

            log.info(f"Found solution on slots {a_rec.scoped_slot_code_list}")
            # TODO for @Xingtong, calculate KPI about recommendation

            result_slot.append(a_rec)
            if len(result_slot) >= max_number_of_matching:
                return result_slot
        # log.info(f"Partial solutions returned")
        return result_slot