Skip to content

Commit

Permalink
docs (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
ProKil authored Oct 25, 2024
1 parent 58e69ad commit 986195e
Show file tree
Hide file tree
Showing 8 changed files with 527 additions and 2 deletions.
50 changes: 50 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: website

# build the documentation whenever there are new commits on main
on:
push:
branches:
- main
- docs/*
# Alternative: only build for tags.
# tags:
# - '*'

# security: restrict permissions for CI jobs.
permissions:
contents: read

jobs:
# Build the documentation and upload the static HTML files as an artifact.
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.13'

# ADJUST THIS: install all dependencies (including pdoc)
- run: pip install uv
# ADJUST THIS: build your documentation into docs/.
# We use a custom build script for pdoc itself, ideally you just run `pdoc -o docs/ ...` here.
- run: uv run --extra doc pdoc --output docs --mermaid aact

- uses: actions/upload-pages-artifact@v3
with:
path: docs/

# Deploy the artifact to GitHub pages.
# This is a separate job so that only actions/deploy-pages has the necessary permissions.
deploy:
needs: build
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v4
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ gui = [
ai = [
"openai"
]
doc = [
"pdoc>=15.0.0, <16.0.0",
]

[build-system]
requires = ["hatchling"]
Expand Down
198 changes: 197 additions & 1 deletion src/aact/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,200 @@
r"""
# What is AAct?
AAct is designed for communicating sensors, neural networks, agents, users, and environments.
<details>
<summary>Can you expand on that?</summary>
AAct is a Python library for building asynchronous, actor-based, concurrent systems.
Specifically, it is designed to be used in the context of building systems with
components that communicate with each other but don't block each other.
</details>
## How does AAct work?
AAct is built around the concept of nodes and dataflow, where nodes are self-contained units
which receive messages from input channels, process the messages, and send messages to output channels.
Nodes are connected to each other to form a dataflow graph, where messages flow from one node to another.
Each node runs in its own event loop, and the nodes communicate with each other using Redis Pub/Sub.
## Why should I use AAct?
1. Non-blocking: the nodes are relatively independent of each other, so if you are waiting for users' input,
you can still process sensor data in the background.
2. Scalable: you can a large number of nodes on one machine or distribute them across multiple machines.
3. Hackable: you can easily design your own nodes and connect them to the existing nodes.
4. Zero-code configuration: the `dataflow.toml` allows you to design the dataflow graph without writing any
Python code.
# Quickstart
## Installation
System requirement:
1. Python 3.10 or higher
2. Redis server
<details>
<summary>Redis installation</summary>
The easiest way to install Redis is to use Docker:
```bash
docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latest
```
According to your system, you can also install Redis from the official website: https://redis.io/download
Note: we will only require a standard Redis server (without RedisJSON / RedisSearch) in this library.
</details>
```bash
pip install aact
```
<details>
<summary> from source </summary>
```bash
git clone https://github.com/ProKil/aact.git
cd aact
pip install .
```
For power users, please use `uv` for package management.
</details>
## Quick Start Example
Assuming your Redis is hosted on `localhost:6379` using docker.
You can create a `dataflow.toml` file:
```toml
redis_url = "redis://localhost:6379/0" # required
[[nodes]]
node_name = "print"
node_class = "print"
[nodes.node_args.print_channel_types]
"tick/secs/1" = "tick"
[[nodes]]
node_name = "tick"
node_class = "tick"
```
To run the dataflow:
```bash
aact run-dataflow dataflow.toml
```
This will start the `tick` node and the `print` node. The `tick` node sends a message every second to the `print` node, which prints the message to the console.
## Usage
### CLI
You can start from CLI and progress to more advanced usages.
1. `aact --help` to see all commands
2. `aact run-dataflow <dataflow_name.toml>` to run a dataflow. Check [Dataflow.toml syntax](#dataflowtoml-syntax)
3. `aact run-node` to run one node in a dataflow.
4. `aact draw-dataflow <dataflow_name_1.toml> <dataflow_name_2.toml> --svg-path <output.svg>` to draw dataflow.
### Customized Node
Here is the minimal knowledge you would need to implement a customized node.
```python
from aact import Node, NodeFactory, Message
@NodeFactory.register("node_name")
class YourNode(Node[your_input_type, your_output_type]):
# event_handler is the only function your **have** to implement
def event_handler(self, input_channel: str, input_message: Message[your_input_type]) -> AsyncIterator[str, Message[your_output_type]]:
match input_channel:
case input_channel_1:
<do_your_stuff>
yield output_channel_1, Message[your_output_type](data=your_output_message)
case input_channel_2:
...
# implement other functions: __init__, _wait_for_input, event_loop, __aenter__, __aexit__
# To run a node without CLI
async with NodeFactory.make("node_name", arg_1, arg_2) as node:
await node.event_loop()
```
## Concepts
There are three important concepts to understand aact.
```mermaid
graph TD
n1[Node 1] -->|channel_1| n2[Node 2]
```
### Nodes
Nodes (`aact.Nodes`) are designed to run in parallel asynchronously. This design is especially useful for deploying the nodes onto different machines.
A node should inherit `aact.Node` class, which extends `pydantic.BaseModel`.
### Channels
Channel is an inherited concept from Redis Pub/Sub. You can think of it as a radio channel.
Multiple publishers (nodes) can publish messages to the same channel, and multiple subscribers (nodes) can subscribe to the same channel.
### Messages
Messages are the data sent through the channels. Each message type is a class in the format of `Message[T]` , where `T` is a subclass or a union of subclasses of `DataModel`.
#### Customized Message Type
If you want to create a new message type, you can create a new class that inherits from `DataModel`.
```python
@DataModelFactory.register("new_type")
class NewType(DataModel):
new_type_data: ... = ...
# For example
@DataModelFactory.register("integer")
class Integer(DataModel):
integer_data: int = Field(default=0)
```
## Dataflow.toml syntax
```toml
redis_url = "redis://..." # required
extra_modules = ["package1.module1", "package2.module2"] # optional
[[nodes]]
node_name = "node_name_1" # A unique name in the dataflow
node_class = "node_class_1" # node_class should match the class name passed into NodeFactory.register
[node.node_args]
node_arg_1 = "value_1"
[[nodes]]
node_name = "node_name_2"
node_class = "node_class_2"
# ...
```
"""

from .nodes import Node, NodeFactory
from .messages import Message

__all__ = ["Node", "NodeFactory", "Message"]
__all__ = ["Node", "NodeFactory", "Message", "nodes", "messages", "cli"]
73 changes: 73 additions & 0 deletions src/aact/messages/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,84 @@


class DataModel(BaseModel):
"""
# DataModel
A datamodel in `aact` is a pydantic BaseModel with an additional field `data_type` to differentiate between different message types.
Here are the built-in data models:
- `aact.message.Tick`: A data model with a single field `tick` of type `int`. This is useful for sending clock ticks.
- `aact.messages.Float`: A data model with a single field `value` of type `float`. This is useful for sending floating-point numbers.
- `aact.messages.Image`: A data model with a single field `image` of type `bytes`. This is useful for sending images.
- `aact.messages.Text`: A data model with a single field `text` of type `str`. This is useful for sending text messages.
- `aact.messages.Audio`: A data model with a single field `audio` of type `bytes`. This is useful for sending audio files.
- `aact.messages.Zero`: A dummy data model with no fields. This is useful when the nodes do not receive or send any data.
## Customize DataModels
For custimizing your own data models, here is an example:
```python
from aact.messages import DataModel, DataModelFactory
@DataModelFactory.register("my_data_model")
class MyDataModel(DataModel):
my_field: str
```
You can see that you don't need to define the `data_type` field in your custom data models. The `DataModelFactory` will take care of it for you.
"""

data_type: Literal[""] = Field("")
"""
@private
"""


T = TypeVar("T", bound=DataModel)


class Message(BaseModel, Generic[T]):
"""
# Messages
Message class is the base class for all of the messages passing through the channels.
It is a pydantic BaseModel with a single field `data` containing the actual data.
The `data` field is a subclass of `aact.messages.DataModel`.
## Usage
To create a message type with DataModel `T`, you can use `Message[T]`.
To initialize a message, you can use `Message[T](data=your_data_model_instance)`.
<details>
<summary> Why have an additional wrapper over DataModel? </summary>
The reason for having a separate class for messages is to leverage the [pydantic's tagged union feature](https://docs.pydantic.dev/latest/concepts/performance/#use-tagged-union-not-union).
This allows us to differentiate between different message types at runtime.
For example, the following code snippet shows how to decide the message type at runtime:
```python
from aact import Message, DataModel
from aact.messages import Image, Tick
tick = 123
tick_message = Message[Tick](data=Tick(tick=tick))
tick_message_json = tick_message.model_dump_json()
possible_image_or_tick_message = Message[Tick | Image].model_validate_json(
tick_message_json
)
assert isinstance(possible_image_or_tick_message.data, Tick)
```
</details>
"""

data: T = Field(discriminator="data_type")
"""
@private
"""
8 changes: 8 additions & 0 deletions src/aact/nodes/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
"""
@public
Nodes are the basic computation units in the AAct library.
A node specifies the input and output channels and types.
All nodes must inherit from the `aact.Node` class.
"""

from .base import Node
from .tick import TickNode
from .random import RandomNode
Expand Down
Loading

0 comments on commit 986195e

Please sign in to comment.