Skip to content
This repository has been archived by the owner on Jun 9, 2023. It is now read-only.

35 handling orchestrator #40

Merged
merged 10 commits into from
Mar 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions clai/datasource/config_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def read_all_user_config(self) -> PluginConfigJson:
config_for_all_users = PluginConfigJson(**loaded)
return config_for_all_users

def read_config(self, user_name: Optional[str]) -> PluginConfig:
def read_config(self, user_name: Optional[str] = None) -> PluginConfig:
selected = None
config_for_all_users = self.read_all_user_config()
if user_name in config_for_all_users.selected:
Expand All @@ -46,17 +46,19 @@ def read_config(self, user_name: Optional[str]) -> PluginConfig:
return PluginConfig(
selected=selected,
default=config_for_all_users.default,
default_orchestrator=config_for_all_users.default_orchestrator,
installed=config_for_all_users.installed,
report_enable=config_for_all_users.report_enable
)

def store_config(self, config: PluginConfig, user_name: str):
def store_config(self, config: PluginConfig, user_name: str = None):
current_config = self.read_all_user_config()
with open(self.get_config_path(), 'w') as json_file:
if user_name:
current_config.selected[user_name] = config.selected
current_config.installed = config.installed
current_config.report_enable = config.report_enable
current_config.orchestrator = config.orchestrator
json_as_string = str(current_config.json())
json_file.write(json_as_string)

Expand Down
10 changes: 8 additions & 2 deletions clai/datasource/model/plugin_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# of this source tree for licensing information.
#

from typing import Dict, List, Union
from typing import Dict, List, Union, Optional

from pydantic import BaseModel

Expand All @@ -14,17 +14,23 @@
class PluginConfig:
def __init__(self,
selected: List[str] = [],
orchestrator: Optional[str] = None,
default: List[str] = [],
installed: List[str] = [],
report_enable: bool = False):
report_enable: bool = False,
default_orchestrator: str = ""):
self.selected = selected
self.default = default
self.installed = installed
self.report_enable = report_enable
self.default_orchestrator = default_orchestrator
self.orchestrator = orchestrator


class PluginConfigJson(BaseModel):
selected: Dict[str, list] = {}
default: Union[List[str], str] = ["demo_agent"]
default_orchestrator: str = "max_orchestrator"
installed: List[str] = []
report_enable: bool = False
orchestrator: Optional[str] = None
75 changes: 75 additions & 0 deletions clai/server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ CLAI comes with a set of orchestrators to help you get the best out of the Orche

> [`max_orchestrator`](orchestration/patterns/max_orchestrator) This picks the skills that responds with the highest confidence, above a certain threshold. This is the default choice as specified [here](../../configPlugins.json).

> [`preference_orchestrator`](orchestration/patterns/preference_orchestrator) picks the skill with highest confidence above a threshold that do not violate any of the partial (ordering) preferences specified by the user.

> [`threshold_orchestrator`](orchestration/patterns/threshold_orchestrator) This is similar to the `max_orchestrator` but it maintains thresholds specific to each skill, and updates them according to how the end user reacts to them.

> [`bandit_orchestrator`](orchestration/patterns/bandit_orchestrator) This learns user preferences using contextual bandits.
Expand All @@ -34,6 +36,79 @@ These are housed in the [orchestration/patterns/](orchestration/patterns) folder

## The Orchestrator API

### Anatomy of an Orchestrator

Every orchestrator you want to make available to CLAI is housed in this directory.

```
<root>
- clai
+ tools
+ emulator
...
- server
+ command_runner
...
- orchestration
- patterns
- pattern_1
- manifest.properties
- install.sh
- pattern_1.py
+ pattern_2
+ pattern_3
...
+ docs
+ bin
+ scripts
...
```

### The Manifest

A `manifest.propertie`s file which documents the name, a brief description, and the default installation status of the orchestration pattern. Here is an [example](./orchestration/patterns/max_orchestrator/manifest.properties).

```
[DEFAULT]
name=dummy pattern
description=This is a dummy pattern
exclude=true
```

+ **name:** This is the name your orchestration pattern.
+ **description:** This is a text description of your pattern (supports ASCII) that appears when you list all the patterns on the command line in verbose form.
+ **exclude:** This is a boolean flag (see above) that you can use to tell CLAI not to list a pattern among those available to the user on the command line. For example, the `bandit_orchestrator` uses an IBM internal package and is hence not listed.

At runtime, you can list available orchestrators like so:

```
>> clai orchestrate
Available Orchestrators:
☑ max_orchestrator
◻ preference_orchestrator
◻ threshold_orchestrator
```

You can use the `-v` flag to print out the desriptions in the manifest. You can also switch between orchestration patterns using the orchestrate command.

```
>> clai orchestrate preference_orchestrator
Available Orchestrators:
◻ max_orchestrator
☑ preference_orchestrator
◻ threshold_orchestrator
```

### The Installer

An `install.sh` file that installs all dependencies required by the orchestrator.
All the orchestrators are installed at the time of installation of CLAI.
Here is an [example](./orchestration/patterns/max_orchestrator/install.sh).

## The Code

Finally the main python file that houses your skill should match the folder name (here is an [example](./orchestration/patterns/max_orchestrator/max_orchestrator.py)). The class name for your skill can be any name as long as it inherits the `Orchestrator` class.

The Orchestrator API implements a posterior orchestration layer for CLAI:

+ Any time an user hits return on the terminal, the command is broadcasted to all the active skills .
Expand Down
16 changes: 16 additions & 0 deletions clai/server/agent_datasource.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def __init__(self, config_storage: ConfigStorage = config):
self.__plugins: Dict[str, Agent] = {}
self.num_workers = 4
self.config_storage = config_storage
self.current_orchestrator = None

@staticmethod
def get_path():
Expand Down Expand Up @@ -176,6 +177,21 @@ def all_plugins(self) -> List[AgentDescriptor]:

return agent_descriptors

def get_current_orchestrator(self) -> str:
if not self.current_orchestrator:
plugin_config = self.config_storage.read_config()
self.current_orchestrator = plugin_config.default_orchestrator
plugin_config.orchestrator = self.current_orchestrator
self.config_storage.store_config(plugin_config)

return self.current_orchestrator

def select_orchestrator(self, orchestrator_name: str):
plugin_config = self.config_storage.read_config()
self.current_orchestrator = orchestrator_name
plugin_config.orchestrator = self.current_orchestrator
self.config_storage.store_config(config)

def get_current_plugin_name(self, user_name: str) -> List[str]:
selected_plugin = self.__get_selected_by_user(user_name)
if selected_plugin is None:
Expand Down
13 changes: 7 additions & 6 deletions clai/server/agent_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,19 @@
from clai.server.command_message import State, Action, TerminalReplayMemory
from clai.server.agent import Agent
from clai.server.agent_executor import thread_executor as agent_executor
from clai.server.orchestration.orchestrator import Orchestrator
from clai.server.orchestration.orchestrator_provider import OrchestratorProvider
from clai.server.orchestration.orchestrator_storage import OrchestratorStorage


class AgentRunner:
def __init__(self, agent_datasource: AgentDatasource,
orchestrator: Orchestrator
orchestrator_provider: OrchestratorProvider
):

self.agent_datasource = agent_datasource
self.orchestrator = orchestrator
self.orchestrator_provider = orchestrator_provider
self.remote_storage = ActionRemoteStorage()
self.orchestrator_storage = OrchestratorStorage(orchestrator, self.remote_storage)
self.orchestrator_storage = OrchestratorStorage(orchestrator_provider, self.remote_storage)

self._pre_exec_id = "pre"
self._post_exec_id = "post"
Expand Down Expand Up @@ -60,7 +60,9 @@ def select_best_candidate(self, command: State, agent_list: List[Agent],
candidate_actions: Optional[List[Union[Action, List[Action]]]],
force_response: bool, pre_post_state: str) -> Optional[Union[Action, List[Action]]]:
agent_names = [agent.agent_name for agent in agent_list]
suggested_command = self.orchestrator.choose_action(

orchestrator = self.orchestrator_provider.get_current_orchestrator()
suggested_command = orchestrator.choose_action(
command=command, agent_names=agent_names, candidate_actions=candidate_actions,
force_response=force_response, pre_post_state=pre_post_state)

Expand Down Expand Up @@ -90,7 +92,6 @@ def process(self,
self.store_pre_orchestrator_memory(command, plugin_instances, candidate_actions, ignore_threshold,
suggested_command)


if isinstance(suggested_command, Action):
if not suggested_command.suggested_command:
suggested_command.suggested_command = command.command
Expand Down
42 changes: 41 additions & 1 deletion clai/server/clai_message_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from clai.server.command_message import Action
from clai.server.command_runner.agent_descriptor import AgentDescriptor
from clai.server.orchestration.orchestrator_descriptor import OrchestratorDescriptor
from clai.tools.colorize_console import Colorize
from clai.tools.process_utils import check_if_process_running

Expand Down Expand Up @@ -53,6 +54,44 @@ def create_error_select(selected_plugin: str) -> Action:
)


def create_orchestrator_list(selected_orchestrator: str,
all_orchestrator: List[OrchestratorDescriptor],
verbose_mode=False) -> Action:
text = 'Available Orchestrators:\n'

for orchestrator in all_orchestrator:
if selected_orchestrator == orchestrator.name:
text += Colorize() \
.emoji(Colorize.EMOJI_CHECK) \
.complete() \
.append(f" {orchestrator.name}\n") \
.to_console()

if verbose_mode:
text += Colorize() \
.complete() \
.append(f" {orchestrator.description}\n") \
.to_console()

else:
text += \
Colorize() \
.emoji(Colorize.EMOJI_BOX) \
.append(f' {orchestrator.name}\n') \
.to_console()

if verbose_mode:
text += Colorize() \
.append(f" {orchestrator.description}\n") \
.to_console()

return Action(
suggested_command=':',
description=text,
execute=True
)


def create_message_list(selected_plugin: List[str], all_plugins: List[AgentDescriptor], verbose_mode=False) -> Action:
text = 'Available Skills:\n'

Expand Down Expand Up @@ -106,10 +145,11 @@ def create_message_server_runing() -> str:
def create_message_help() -> Action:
text = Colorize().info() \
.append("CLAI usage:\n"
"clai [help] [skills [-v]] [activate [skill_name]] [deactivate [skill_name]] "
"clai [help] [skills [-v]] [orchestrate [name]] [activate [skill_name]] [deactivate [skill_name]] "
"[manual | automatic] [install [name | url]] \n\n"
"help Print help and usage of clai.\n"
"skills List available skills. Use -v For a verbose description of each skill.\n"
"orchestrate Activate the orchestrator by name. If name is empty, list available orchestrators.\n"
"activate Activate the named skill.\n"
"deactivate Deactivate the named skill.\n"
"manual Disables automatic execution of commands without operator confirmation.\n"
Expand Down
41 changes: 41 additions & 0 deletions clai/server/command_runner/clai_orchestrate_command_runner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#
# Copyright (C) 2020 IBM. All Rights Reserved.
#
# See LICENSE.txt file in the root directory
# of this source tree for licensing information.
#

from clai.server.clai_message_builder import create_orchestrator_list
from clai.server.command_message import State, Action
from clai.server.command_runner.clean_input import extract_quoted_agent_name
from clai.server.command_runner.command_runner import CommandRunner

# pylint: disable=too-few-public-methods
from clai.server.orchestration.orchestrator_provider import OrchestratorProvider


class ClaiOrchestrateCommandRunner(CommandRunner):
SELECT_DIRECTIVE = 'clai orchestrate'
__VERBOSE_MODE = '-v'

def __init__(self, orchestrator_provider: OrchestratorProvider):
self.orchestrator_provider = orchestrator_provider

def execute(self, state: State) -> Action:
orchestrator_to_select = state.command.replace(f'{self.SELECT_DIRECTIVE}', '').strip()

verbose = False
if self.__VERBOSE_MODE in orchestrator_to_select:
verbose = True
orchestrator_to_select = ""
else:
orchestrator_to_select = extract_quoted_agent_name(orchestrator_to_select)

if orchestrator_to_select:
self.orchestrator_provider.select_orchestrator(orchestrator_to_select)

return create_orchestrator_list(
self.orchestrator_provider.get_current_orchestrator_name(),
self.orchestrator_provider.all_orchestrator(),
verbose
)
6 changes: 5 additions & 1 deletion clai/server/command_runner/command_runner_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@
from clai.server.command_runner.clai_help_command_runner import ClaiHelpCommandRunner
from clai.server.command_runner.clai_install_command_runner import ClaiInstallCommandRunner
from clai.server.command_runner.clai_last_info_command_runner import ClaiLastInfoCommandRunner
from clai.server.command_runner.clai_orchestrate_command_runner import ClaiOrchestrateCommandRunner
from clai.server.command_runner.clai_plugins_command_runner import ClaiPluginsCommandRunner
from clai.server.command_runner.clai_power_command_runner import ClaiPowerCommandRunner
from clai.server.command_runner.clai_power_disable_command_runner import ClaiPowerDisableCommandRunner
from clai.server.command_runner.clai_reload_command_runner import ClaiReloadCommandRunner
from clai.server.command_runner.clai_select_command_runner import ClaiSelectCommandRunner
from clai.server.command_runner.clai_unselect_command_runner import ClaiUnselectCommandRunner
from clai.server.command_runner.command_runner import CommandRunner, PostCommandRunner
from clai.server.orchestration.orchestrator_provider import OrchestratorProvider

CLAI_COMMAND_NAME = "clai"

Expand All @@ -32,11 +34,13 @@ class CommandRunnerFactory:
def __init__(self,
agent_datasource: AgentDatasource,
config_storage: ConfigStorage,
server_status_datasource: ServerStatusDatasource
server_status_datasource: ServerStatusDatasource,
orchestrator_provider: OrchestratorProvider
):
self.server_status_datasource = server_status_datasource
self.clai_commands: Dict[str, CommandRunner] = {
"skills": ClaiPluginsCommandRunner(agent_datasource),
"orchestrate": ClaiOrchestrateCommandRunner(orchestrator_provider),
"activate": ClaiSelectCommandRunner(config_storage, agent_datasource),
"deactivate": ClaiUnselectCommandRunner(config_storage, agent_datasource),
"manual": ClaiPowerDisableCommandRunner(server_status_datasource),
Expand Down
Loading