-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Rewrite Agent developer guide (#279)
* Write barebones agent * Rewrite Agent developer guide * Incorporate feedback from @erfz
- Loading branch information
1 parent
e2670f3
commit d22e6ae
Showing
20 changed files
with
2,449 additions
and
761 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
# OCS Barebones Agent | ||
# ocs Agent for demonstrating how to write an Agent | ||
|
||
# Use ocs base image | ||
FROM ocs:latest | ||
|
||
# Set the working directory to registry directory | ||
WORKDIR /app/ocs/agents/barebones_agent/ | ||
|
||
# Copy this agent into the WORKDIR | ||
COPY . . | ||
|
||
# Run registry on container startup | ||
ENTRYPOINT ["dumb-init", "python3", "-u", "barebones_agent.py"] | ||
|
||
# Sensible defaults for crossbar server | ||
CMD ["--site-hub=ws://crossbar:8001/ws", \ | ||
"--site-http=http://crossbar:8001/call"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,190 @@ | ||
import time | ||
import txaio | ||
|
||
from os import environ | ||
|
||
from ocs import ocs_agent, site_config | ||
from ocs.ocs_twisted import TimeoutLock | ||
|
||
|
||
class BarebonesAgent: | ||
"""Barebone Agent demonstrating writing an Agent from scratch. | ||
This Agent is meant to be an example for Agent development, and provides a | ||
clean starting point when developing a new Agent. | ||
Parameters: | ||
agent (OCSAgent): OCSAgent object from :func:`ocs.ocs_agent.init_site_agent`. | ||
Attributes: | ||
agent (OCSAgent): OCSAgent object from :func:`ocs.ocs_agent.init_site_agent`. | ||
log (txaio.tx.Logger): Logger object used to log events within the | ||
Agent. | ||
lock (TimeoutLock): TimeoutLock object used to prevent simultaneous | ||
commands being sent to hardware. | ||
_count (bool): Internal tracking of whether the Agent should be | ||
counting or not. This is used to exit the Process loop by changing | ||
it to False via the count.stop() command. Your Agent won't use this | ||
exact attribute, but might have a similar one. | ||
""" | ||
|
||
def __init__(self, agent): | ||
self.agent = agent | ||
self.log = agent.log | ||
self.lock = TimeoutLock(default_timeout=5) | ||
self._count = False | ||
|
||
# Register OCS feed | ||
agg_params = { | ||
'frame_length': 10 * 60 # [sec] | ||
} | ||
self.agent.register_feed('feed_name', | ||
record=True, | ||
agg_params=agg_params, | ||
buffer_time=1.) | ||
|
||
def count(self, session, params): | ||
"""count(test_mode=False) | ||
**Process** - Count up from 0. | ||
The count will restart if the process is stopped and restarted. | ||
Notes: | ||
The most recent value is stored in the session data object in the | ||
format:: | ||
>>> response.session['data'] | ||
{"value": 0, | ||
"timestamp":1600448753.9288929} | ||
""" | ||
with self.lock.acquire_timeout(timeout=0, job='count') as acquired: | ||
if not acquired: | ||
print("Lock could not be acquired because it " + | ||
f"is held by {self.lock.job}") | ||
return False | ||
|
||
session.set_status('running') | ||
|
||
# Initialize last release time for lock | ||
last_release = time.time() | ||
|
||
# Initialize the counter | ||
self._count=True | ||
counter = 0 | ||
|
||
self.log.info("Starting the count!") | ||
|
||
# Main process loop | ||
while self._count: | ||
# About every second, release and acquire the lock | ||
if time.time() - last_release > 1.: | ||
last_release = time.time() | ||
if not self.lock.release_and_acquire(timeout=10): | ||
print(f"Could not re-acquire lock now held by {self.lock.job}.") | ||
return False | ||
|
||
# Perform the process actions | ||
counter += 1 | ||
self.log.debug(f"{counter}! Ah! Ah! Ah!") | ||
now = time.time() | ||
session.data = {"value": counter, | ||
"timestamp": now} | ||
|
||
# Format message for publishing to Feed | ||
message = {'block_name': 'count', | ||
'timestamp': now, | ||
'data': {'value': counter}} | ||
self.agent.publish_to_feed('feed_name', message) | ||
time.sleep(1) | ||
|
||
self.agent.feeds['feed_name'].flush_buffer() | ||
|
||
return True, 'Acquisition exited cleanly.' | ||
|
||
def _stop_count(self, session, params): | ||
"""Stop monitoring the turbo output.""" | ||
if self._count: | ||
self._count = False | ||
return True, 'requested to stop taking data.' | ||
else: | ||
return False, 'count is not currently running' | ||
|
||
@ocs_agent.param('text', default='hello world', type=str) | ||
def print(self, session, params): | ||
"""print(text='hello world') | ||
**Task** - Print some text passed to a Task. | ||
Args: | ||
text (str): Text to print out. Defaults to 'hello world'. | ||
Notes: | ||
The session data will be updated with the text:: | ||
>>> response.session['data'] | ||
{'text': 'hello world', | ||
'last_updated': 1660249321.8729222} | ||
""" | ||
with self.lock.acquire_timeout(timeout=3.0, job='print') as acquired: | ||
if not acquired: | ||
self.log.warn("Lock could not be acquired because it " + | ||
f"is held by {self.lock.job}") | ||
return False | ||
|
||
# Set operations status to 'running' | ||
session.set_status('running') | ||
|
||
# Log the text provided to the Agent logs | ||
self.log.info(f"{params['text']}") | ||
|
||
# Store the text provided in session.data | ||
session.data = {'text': params['text'], | ||
'last_updated': time.time()} | ||
|
||
# bool, 'descriptive text message' | ||
# True if task succeeds, False if not | ||
return True, 'Printed text to logs' | ||
|
||
|
||
def add_agent_args(parser_in=None): | ||
if parser_in is None: | ||
from argparse import ArgumentParser as A | ||
parser_in = A() | ||
pgroup = parser_in.add_argument_group('Agent Options') | ||
pgroup.add_argument('--mode', type=str, default='count', | ||
choices=['idle', 'count'], | ||
help="Starting action for the Agent.") | ||
|
||
return parser_in | ||
|
||
|
||
if __name__ == '__main__': | ||
# For logging | ||
txaio.use_twisted() | ||
LOG = txaio.make_logger() | ||
|
||
# Start logging | ||
txaio.start_logging(level=environ.get("LOGLEVEL", "info")) | ||
|
||
parser = add_agent_args() | ||
args = site_config.parse_args(agent_class='BarebonesAgent', parser=parser) | ||
|
||
startup = False | ||
if args.mode == 'count': | ||
startup = True | ||
|
||
agent, runner = ocs_agent.init_site_agent(args) | ||
|
||
barebone = BarebonesAgent(agent) | ||
agent.register_process( | ||
'count', | ||
barebone.count, | ||
barebone._stop_count, | ||
startup=startup) | ||
agent.register_task('print', barebone.print) | ||
|
||
runner.run(agent, auto_reconnect=True) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
.. _barebones: | ||
|
||
=============== | ||
Barebones Agent | ||
=============== | ||
|
||
The Barebones Agent is provided with OCS to provide a starting point for Agent | ||
development. It is heavily used throughout the Agent development guide. | ||
|
||
Configuration File Examples | ||
--------------------------- | ||
|
||
Below are configuration examples for the ocs config file and for running the | ||
Agent in a docker container. | ||
|
||
OCS Site Config | ||
``````````````` | ||
|
||
To configure the Fake Data Agent we need to add a FakeDataAgent block to our | ||
ocs configuration file. Here is an example configuration block using all of the | ||
available arguments:: | ||
|
||
{'agent-class': 'BarebonesAgent', | ||
'instance-id': 'barebones-1', | ||
'arguments': []}, | ||
|
||
Agent API | ||
--------- | ||
|
||
.. autoclass:: agents.barebones_agent.barebones_agent.BarebonesAgent | ||
:members: |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
.. _documentation: | ||
|
||
Documentation | ||
------------- | ||
|
||
Documentation is important for users writing OCSClients that can interact with | ||
your new Agent. When writing a new Agent you must document the Tasks and | ||
Processes with appropriate docstrings. Additionally a page must be created | ||
within the docs to describe the Agent and provide other key information such as | ||
configuration file examples. You should aim to be a thorough as possible when | ||
writing documentation for your Agent. | ||
|
||
Task and Process Documentation | ||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
Each Task and Process within an Agent must be accompanied by a docstring. Here | ||
is a complete example of a well documented Task (or Process):: | ||
|
||
@ocs_agent.param('arg1', type=bool) | ||
@ocs_agent.param('arg2', default=7, type=int) | ||
def demo(self, session, params): | ||
"""demo(arg1=None, arg2=7) | ||
|
||
**Task** (or **Process**) - An example task docstring for illustration purposes. | ||
|
||
Parameters: | ||
arg1 (bool): Useful argument 1. | ||
arg2 (int, optional): Useful argument 2, defaults to 7. For details see | ||
:func:`socs.agent.demo_agent.DemoClass.detailing_method` | ||
|
||
Examples: | ||
Example for calling in a client:: | ||
|
||
client.demo(arg1=False, arg2=5) | ||
|
||
Notes: | ||
An example of the session data:: | ||
|
||
>>> response.session['data'] | ||
{"fields": | ||
{"Channel_05": {"T": 293.644, "R": 33.752, "timestamp": 1601924482.722671}, | ||
"Channel_06": {"T": 0, "R": 1022.44, "timestamp": 1601924499.5258765}, | ||
"Channel_08": {"T": 0, "R": 1026.98, "timestamp": 1601924494.8172355}, | ||
"Channel_01": {"T": 293.41, "R": 108.093, "timestamp": 1601924450.9315426}, | ||
"Channel_02": {"T": 293.701, "R": 30.7398, "timestamp": 1601924466.6130798} | ||
} | ||
} | ||
""" | ||
pass | ||
|
||
Keep reading for more details on what's going on in this example. | ||
|
||
Overriding the Method Signature | ||
``````````````````````````````` | ||
``session`` and ``params`` are both required parameters when writing an OCS | ||
Task or Process, but both should be hidden from users writing OCSClients. When | ||
documenting a Task or Process, the method signature should be overridden to | ||
remove both ``session`` and ``params``, and to include any parameters your Task | ||
or Process might take. This is done in the first line of the docstring, by | ||
writing the method name, followed by the parameters in parentheses. In the | ||
above example that looks like:: | ||
|
||
def demo(self, session, params=None): | ||
"""demo(arg1=None, arg2=7)""" | ||
|
||
This will render the method description as ``delay_task(arg1=None, | ||
arg2=7)`` within Sphinx, rather than ``delay_task(session, params=None)``. The | ||
default values should be put in this documentation. If a parameter is required, | ||
set the param to ``None`` in the method signature. For more info on the | ||
``@ocs_agent.param`` decorator see :ref:`param`. | ||
|
||
Keyword Arguments | ||
````````````````` | ||
Internal to OCS the keyword arguments provided to an OCSClient are passed as a | ||
`dict` to ``params``. For the benefit of the end user, these keyword arguments | ||
should be documented in the Agent as if passed as such. So the docstring should | ||
look like:: | ||
|
||
Parameters: | ||
arg1 (bool): Useful argument 1. | ||
arg2 (int, optional): Useful argument 2, defaults to 7. For details see | ||
:func:`socs.agent.lakeshore.LakeshoreClass.the_method` | ||
|
||
Examples | ||
```````` | ||
Examples should be given using the "Examples" heading when it would improve the | ||
clarity of how to interact with a given Task or Process:: | ||
|
||
Examples: | ||
Example for calling in a client:: | ||
|
||
client.demo(arg1=False, arg2=5) | ||
|
||
Session Data | ||
```````````` | ||
The ``session.data`` object structure is left up to the Agent author. As such, | ||
it needs to be documented so that OCSClient authors know what to expect. If | ||
your Task or Process makes use of ``session.data``, provide an example of the | ||
structure under the "Notes" heading. On the OCSClient end, this | ||
``session.data`` object is returned in the response under | ||
``response.session['data']``. This is how it should be presented in the example | ||
docstrings:: | ||
|
||
Notes: | ||
An example of the session data:: | ||
|
||
>>> response.session['data'] | ||
{"fields": | ||
{"Channel_05": {"T": 293.644, "R": 33.752, "timestamp": 1601924482.722671}, | ||
"Channel_06": {"T": 0, "R": 1022.44, "timestamp": 1601924499.5258765}, | ||
"Channel_08": {"T": 0, "R": 1026.98, "timestamp": 1601924494.8172355}, | ||
"Channel_01": {"T": 293.41, "R": 108.093, "timestamp": 1601924450.9315426}, | ||
"Channel_02": {"T": 293.701, "R": 30.7398, "timestamp": 1601924466.6130798} | ||
} | ||
} | ||
|
||
For more details on the ``session.data`` object see :ref:`session_data`. | ||
|
||
Agent Reference Pages | ||
^^^^^^^^^^^^^^^^^^^^^ | ||
Now that you have documented your Agent's Tasks and Processes appropriately we | ||
need to make the page that will display that documentation. Agent reference | ||
pages are kept in `ocs/docs/agents/ | ||
<https://github.com/simonsobs/ocs/tree/develop/docs/agents>`_. Each Agent has a | ||
separate `.rst` file. Each Agent reference page must contain: | ||
|
||
* Brief description of the Agent | ||
* Example ocs-site-config configuration block | ||
* Example docker-compose configuration block (if Agent is dockerized) | ||
* Agent API reference | ||
|
||
Reference pages can also include: | ||
|
||
* Detailed description of Agent or related material | ||
* Example client scripts | ||
* Supporting APIs | ||
|
||
Here is a template for an Agent documentation page. Text starting with a '#' is | ||
there to guide you in writing the page and should be replaced or removed. | ||
Unneeded sections should be removed. | ||
|
||
.. include:: ../../example/docs/agent_template.rst | ||
:code: rst | ||
|
Oops, something went wrong.