Skip to content

Commit

Permalink
Merge pull request #943 from fetchai/fix/skill-guide
Browse files Browse the repository at this point in the history
Fixing skill-guide
  • Loading branch information
DavidMinarsch authored Mar 18, 2020
2 parents e1fc780 + 822b7f6 commit d57b7cf
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 94 deletions.
98 changes: 31 additions & 67 deletions docs/skill-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,20 @@ Follow the <a href="../quickstart/#preliminaries">Preliminaries</a> and <a href=

We will first create an AEA and add a scaffold skill, which we call `my_search`.

```bash
``` bash
aea create my_aea && cd my_aea
aea scaffold skill my_search
```

In the following steps, we replace each one of the scaffolded `Behaviour`, `Handler` and `Task` in `my_aea/skills/my_search` with our implementation. We will build a simple skill which lets the AEA send a search query to the [OEF](../oef-ledger) and process the resulting response.
In the following steps, we replace the scaffolded `Behaviour` and `Handler` in `my_aea/skills/my_search` with our implementation. We will build a simple skill which lets the AEA send a search query to the [OEF](../oef-ledger) and process the resulting response.

## Step 2: Develop a Behaviour

A `Behaviour` class contains the business logic specific to initial actions initiated by the AEA rather than reactions to other events.

In this example, we implement a simple search behaviour. Each time, `act()` gets called by the main agent loop, we will send a search request to the OEF.

```python
``` python
from aea.helpers.search.models import Constraint, ConstraintType, Query
from aea.skills.behaviours import TickerBehaviour

Expand Down Expand Up @@ -97,7 +97,7 @@ So far, we have tasked the AEA with sending search requests to the OEF. However,

Let us now implement a handler to deal with the incoming search responses.

```python
``` python
from aea.skills.base import Handler

from packages.fetchai.protocols.oef.message import OEFMessage
Expand Down Expand Up @@ -134,6 +134,13 @@ class MySearchHandler(Handler):
self.context.agent_name, nb_agents_found, self.received_search_count
)
)
self.context.logger.info(
"[{}]: number of search requests sent={} vs. number of search responses received={}".format(
self.context.agent_name,
self.context.behaviours.my_search_behaviour.sent_search_count,
self.received_search_count,
)
)

def teardown(self) -> None:
"""
Expand All @@ -148,67 +155,25 @@ class MySearchHandler(Handler):

We create a handler which is registered for the `oef` protocol. Whenever it receives a search result, we log the number of agents returned in the search - the agents matching the search query - and update the counter of received searches.

Note, how the handler simply reacts to incoming events (i.e. messages). It could initiate further actions, however, they are still reactions to the upstream search event.

We place this code in `my_aea/skills/my_search/handlers.py`.

## Step 4: Develop a Task

We have implemented a behaviour and a handler. We conclude by implementing a task. Here we can implement background logic. We will implement a trivial check on the difference between the amount of search requests sent and responses received.

```python
import logging
import time

from aea.skills.tasks import Task
We also implement a trivial check on the difference between the amount of search requests sent and responses received.

logger = logging.getLogger("aea.my_search_skill")


class MySearchTask(Task):
"""This class scaffolds a task."""

def setup(self) -> None:
"""
Implement the setup.
:return: None
"""
logger.info("[{}]: setting up MySearchTask".format(self.context.agent_name))
Note, how the handler simply reacts to incoming events (i.e. messages). It could initiate further actions, however, they are still reactions to the upstream search event.

def execute(self) -> None:
"""
Implement the task execution.
Also note, how we have access to other objects in the skill via `self.context`.

:return: None
"""
time.sleep(1) # to slow down the AEA
logger.info(
"[{}]: number of search requests sent={} vs. number of search responses received={}".format(
self.context.agent_name,
self.context.behaviours.my_search_behaviour.sent_search_count,
self.context.handlers.my_search_handler.received_search_count,
)
)

def teardown(self) -> None:
"""
Implement the task teardown.
We place this code in `my_aea/skills/my_search/handlers.py`.

:return: None
"""
logger.info("[{}]: tearing down MySearchTask".format(self.context.agent_name))
```
## Step 4: Remove unused Task and Model

Note, how we have access to other objects in the skill via `self.context`.
We have implemented a behaviour and a handler. We could also implement a `task` and a `model`, but instead we delete these files in this case, to keep it simple.

We place this code in `my_aea/skills/my_search/tasks.py`.
We remove the files `my_aea/skills/my_search/tasks.py` and `my_aea/skills/my_search/my_model.py`.

## Step 5: Create the config file

Based on our skill components above, we create the following config file.

```yaml
``` yaml
name: my_search
author: fetchai
version: 0.1.0
Expand All @@ -223,34 +188,39 @@ handlers:
my_search_handler:
class_name: MySearchHandler
args: {}
my_search_task:
class_name: MySearchTask
args: {}
models: {}
protocols: ['fetchai/oef:0.1.0']
dependencies: {}
```
Importantly, the keys `my_search_behaviour` and `my_search_handler` are used in the above task to access these skill components at runtime. We also set the `tick_interval` of the `TickerBehaviour` to `5` seconds.
Ensure, you replace the author field with your author name! (Run `aea init` to set or check the author name.)

Importantly, the keys `my_search_behaviour` and `my_search_handler` are used in the above handler to access these skill components at runtime via the context. We also set the `tick_interval` of the `TickerBehaviour` to `5` seconds.

We place this code in `my_aea/skills/my_search/skill.yaml`.

## Step 6: Add the oef protocol and connection

Our AEA does not have the oef protocol yet so let's add it.
```bash
``` bash
aea add protocol fetchai/oef:0.1.0
```

This adds the protocol to our AEA and makes it available on the path `packages.fetchai.protocols...`.

We also need to add the oef connection:
```bash
``` bash
aea add connection fetchai/oef:0.1.0
```

## Step 7: Run a service provider AEA

We first start an oef node (see the <a href="../connection/" target=_blank>connection section</a> for more details) in a separate terminal window.

``` bash
python scripts/oef/launch.py -c ./scripts/oef/launch_config.json
```

In order to be able to find another AEA when searching, from a different terminal window, we fetch and run another finished AEA:
```
aea fetch fetchai/simple_service_registration:0.1.0 && cd simple_service_registration
Expand All @@ -261,19 +231,13 @@ This AEA will simply register a location service on the OEF so we can search for

## Step 8: Run the Search AEA

We first start an oef node (see the <a href="../connection/" target=_blank>connection section</a> for more details) in a separate terminal window.

```bash
python scripts/oef/launch.py -c ./scripts/oef/launch_config.json
```

We can then launch our AEA.

```bash
aea run --connections fetchai/oef:0.1.0
```

We can see that the AEA sends search requests to the OEF and receives search responses from the OEF. Since our AEA is only searching on the OEF - and not registered on the OEF - the search response returns an empty list of agents.
We can see that the AEA sends search requests to the OEF and receives search responses from the OEF. Since our AEA is only searching on the OEF - and not registered on the OEF - the search response returns a single agent (the service provider).

We stop the AEA with `CTRL + C`.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ license: Apache-2.0
logging_config:
disable_existing_loggers: false
version: 1
private_key_paths:
ethereum: eth_private_key.txt
fetchai: fet_private_key.txt
private_key_paths: {}
protocols:
- fetchai/default:0.1.0
- fetchai/fipa:0.1.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

from packages.fetchai.protocols.oef.message import OEFMessage
from packages.fetchai.protocols.oef.serialization import DEFAULT_OEF, OEFSerializer
from packages.fetchai.skills.weather_station.strategy import Strategy
from packages.fetchai.skills.simple_service_registration.strategy import Strategy

SERVICE_ID = ""
DEFAULT_SERVICES_INTERVAL = 30.0
Expand Down
35 changes: 35 additions & 0 deletions tests/test_docs/test_bash_yaml/md_files/bash-skill-guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
``` bash
aea create my_aea && cd my_aea
aea scaffold skill my_search
```
``` yaml
name: my_search
author: fetchai
version: 0.1.0
license: Apache-2.0
description: 'A simple search skill utilising the OEF.'
behaviours:
my_search_behaviour:
class_name: MySearchBehaviour
args:
tick_interval: 5
handlers:
my_search_handler:
class_name: MySearchHandler
args: {}
models: {}
protocols: ['fetchai/oef:0.1.0']
dependencies: {}
```
``` bash
aea add protocol fetchai/oef:0.1.0
```
``` bash
aea add connection fetchai/oef:0.1.0
```
``` bash
python scripts/oef/launch.py -c ./scripts/oef/launch_config.json
```
```bash
aea run --connections fetchai/oef:0.1.0
```
89 changes: 66 additions & 23 deletions tests/test_docs/test_skill_guide/test_skill_guide.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,28 +68,42 @@ def setup_class(cls):
cls.cwd = os.getcwd()
cls.t = tempfile.mkdtemp()

# add packages folder
packages_src = os.path.join(cls.cwd, "packages")
packages_dst = os.path.join(cls.t, "packages")
shutil.copytree(packages_src, packages_dst)

cls.schema = json.load(open(SKILL_CONFIGURATION_SCHEMA))
cls.resolver = jsonschema.RefResolver(
"file://{}/".format(Path(CONFIGURATION_SCHEMA_DIR).absolute()), cls.schema
)
cls.validator = Draft4Validator(cls.schema, resolver=cls.resolver)

os.chdir(cls.t)
cls.create_result = cls.runner.invoke(
cls.init_result = cls.runner.invoke(
cli, [*CLI_LOG_OPTION, "init", "--author", AUTHOR], standalone_mode=False
)
cls.fetch_result = cls.runner.invoke(
cli,
[*CLI_LOG_OPTION, "fetch", "fetchai/simple_service_registration:0.1.0"],
standalone_mode=False,
)
cls.create_result = cls.runner.invoke(
cli, [*CLI_LOG_OPTION, "create", cls.agent_name], standalone_mode=False
)
if cls.create_result.exit_code == 0:
os.chdir(cls.agent_name)
os.chdir(Path(cls.t, cls.agent_name))
# scaffold skill
cls.result = cls.runner.invoke(
cli,
[*CLI_LOG_OPTION, "scaffold", "skill", cls.resource_name],
standalone_mode=False,
)

def test_agent_is_fetched(self):
"""Test that the setup was successful."""
assert self.fetch_result.exit_code == 0, "Agent not fetched, setup incomplete"

def test_agent_is_created(self):
"""Test that the setup was successful."""
assert self.create_result.exit_code == 0, "Agent not created, setup incomplete"
Expand Down Expand Up @@ -125,36 +139,65 @@ def test_update_skill_and_run(self, pytestconfig):
file.write(self.code_blocks[1])

path = Path(self.t, self.agent_name, "skills", self.resource_name, "tasks.py")
with open(path, "w+") as file:
file.write(self.code_blocks[2])
os.remove(path)

path = Path(
self.t, self.agent_name, "skills", self.resource_name, "my_model.py"
)
os.remove(path)

# Update the yaml file.
path = Path(self.t, self.agent_name, "skills", self.resource_name, "skill.yaml")
yaml_code_block = extract_code_blocks(self.path, filter="yaml")
with open(path, "w") as file:
file.write(yaml_code_block[0])

# run the agent
process_one = subprocess.Popen( # nosec
[sys.executable, "-m", "aea.cli", "run"],
stdout=subprocess.PIPE,
env=os.environ.copy(),
)

time.sleep(5.0)
process_one.send_signal(signal.SIGINT)
process_one.wait(timeout=5)
os.chdir(Path(self.t, "simple_service_registration"))
try:
# run service agent
process_one = subprocess.Popen( # nosec
[sys.executable, "-m", "aea.cli", "run"],
stdout=subprocess.PIPE,
env=os.environ.copy(),
)

poll_one = process_one.poll()
if poll_one is None:
process_one.terminate()
process_one.wait(2)
# run the agent
os.chdir(Path(self.t, self.agent_name))
process_two = subprocess.Popen( # nosec
[sys.executable, "-m", "aea.cli", "run"],
stdout=subprocess.PIPE,
env=os.environ.copy(),
)

os.chdir(self.t)
result = self.runner.invoke(
cli, [*CLI_LOG_OPTION, "delete", self.agent_name], standalone_mode=False
)
assert result.exit_code == 0
time.sleep(7.0)
process_one.send_signal(signal.SIGINT)
process_two.send_signal(signal.SIGINT)
process_one.wait(timeout=5)
process_two.wait(timeout=5)
assert process_one.returncode == 0
assert process_two.returncode == 0
finally:
poll_one = process_one.poll()
if poll_one is None:
process_one.terminate()
process_one.wait(2)

poll_two = process_two.poll()
if poll_two is None:
process_two.terminate()
process_two.wait(2)

os.chdir(self.t)
result = self.runner.invoke(
cli, [*CLI_LOG_OPTION, "delete", self.agent_name], standalone_mode=False
)
assert result.exit_code == 0
result = self.runner.invoke(
cli,
[*CLI_LOG_OPTION, "delete", "simple_service_registration"],
standalone_mode=False,
)
assert result.exit_code == 0

@classmethod
def teardown_class(cls):
Expand Down

0 comments on commit d57b7cf

Please sign in to comment.