Skip to content

Commit f169744

Browse files
authored
Merge pull request #24 from aurelio-labs/feat/graph-state
feat: v1 of graph state
2 parents e100f6a + 0bd8f4e commit f169744

10 files changed

Lines changed: 2698 additions & 3300 deletions

File tree

.github/workflows/release.yml

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,34 +9,35 @@ jobs:
99
build:
1010
runs-on: ubuntu-latest
1111
steps:
12-
- uses: actions/checkout@v2
13-
- name: Set up Python
14-
uses: actions/setup-python@v2
12+
- uses: actions/checkout@v4
13+
- name: Install uv
14+
uses: astral-sh/setup-uv@v5
1515
with:
16-
python-version: '3.13'
17-
- name: Install Poetry
18-
run: |
19-
curl -sSL https://install.python-poetry.org | python - -y --version 1.8.4
16+
enable-cache: true
17+
cache-dependency-glob: "uv.lock"
18+
python-version: "3.10"
2019
- name: Install dependencies
21-
run: poetry install
20+
run: uv sync --extra dev
2221
- name: Build
23-
run: poetry build
22+
run: uv build
2423

2524
publish:
2625
needs: build
2726
runs-on: ubuntu-latest
2827
steps:
29-
- uses: actions/checkout@v2
30-
- name: Set up Python
31-
uses: actions/setup-python@v2
28+
- uses: actions/checkout@v4
29+
- name: Install uv
30+
uses: astral-sh/setup-uv@v5
3231
with:
33-
python-version: '3.13'
34-
- name: Install Poetry
35-
run: |
36-
curl -sSL https://install.python-poetry.org | python - -y --version 1.8.4
32+
enable-cache: true
33+
cache-dependency-glob: "uv.lock"
34+
python-version: "3.10"
35+
- name: Install dependencies
36+
run: uv sync --extra dev
37+
- name: Build
38+
run: uv build
3739
- name: Publish to PyPI
3840
run: |
39-
poetry config repositories.remote https://upload.pypi.org/legacy/
40-
poetry --no-interaction -v publish --build --repository remote --username "__token__" --password "$PYPI_API_TOKEN"
41+
uv publish --username "__token__" --password "$PYPI_API_TOKEN"
4142
env:
4243
PYPI_API_TOKEN: ${{ secrets.PYPI_API_TOKEN }}

.github/workflows/test.yml

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ name: Test
33
on:
44
pull_request:
55

6-
76
env:
8-
POETRY_VERSION: "1.8.4"
7+
UV_VERSION: "0.1.19"
98

109
jobs:
1110
build:
1211
runs-on: ubuntu-latest
12+
env:
13+
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
1314
strategy:
1415
matrix:
1516
python-version:
@@ -19,31 +20,19 @@ jobs:
1920
- "3.13"
2021
steps:
2122
- uses: actions/checkout@v4
22-
- name: Cache Poetry
23-
uses: actions/cache@v4
24-
with:
25-
path: ~/.poetry
26-
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
27-
restore-keys: |
28-
${{ runner.os }}-poetry-
29-
- name: Install poetry
30-
run: |
31-
pipx install poetry==$POETRY_VERSION
32-
- name: Set up Python ${{ matrix.python-version }}
33-
uses: actions/setup-python@v4
23+
- name: Install uv
24+
uses: astral-sh/setup-uv@v5
3425
with:
26+
enable-cache: true
27+
cache-dependency-glob: "uv.lock"
3528
python-version: ${{ matrix.python-version }}
36-
cache: poetry
37-
- name: Install dependencies
38-
run: |
39-
poetry install --all-extras
29+
- name: Install Dependencies
30+
run: uv sync --extra dev
4031
- name: Pytest All
4132
run: |
42-
poetry run pytest -vv -n 20 tests
33+
uv run pytest -vv -n 20 tests
4334
- name: Upload coverage to Codecov
4435
uses: codecov/codecov-action@v2
45-
env:
46-
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
4736
with:
4837
file: ./coverage.xml
4938
fail_ci_if_error: false

.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.12

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,10 @@
33
1. Async-first
44
2. Minimize abstractions
55
3. One way to do one thing
6-
4. Graph-based AI
6+
4. Graph-based AI
7+
8+
## Installation
9+
10+
```
11+
pip install -qU graphai-lib
12+
```

graphai/graph.py

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,36 @@
1-
from typing import List, Dict, Any
1+
from typing import List, Dict, Any, Optional
22
from graphai.nodes.base import _Node
33
from graphai.callback import Callback
44
from semantic_router.utils.logger import logger
55

66

77
class Graph:
8-
def __init__(self, max_steps: int = 10):
9-
self.nodes = {}
10-
self.edges = []
11-
self.start_node = None
12-
self.end_nodes = []
8+
def __init__(self, max_steps: int = 10, initial_state: Optional[Dict[str, Any]] = None):
9+
self.nodes: Dict[str, _Node] = {}
10+
self.edges: List[Any] = []
11+
self.start_node: Optional[_Node] = None
12+
self.end_nodes: List[_Node] = []
1313
self.Callback = Callback
1414
self.callback = None
1515
self.max_steps = max_steps
16+
self.state = initial_state or {}
17+
18+
# Allow getting and setting the graph's internal state
19+
def get_state(self) -> Dict[str, Any]:
20+
"""Get the current graph state."""
21+
return self.state
22+
23+
def set_state(self, state: Dict[str, Any]):
24+
"""Set the graph state."""
25+
self.state = state
26+
27+
def update_state(self, values: Dict[str, Any]):
28+
"""Update the graph state with new values."""
29+
self.state.update(values)
30+
31+
def reset_state(self):
32+
"""Reset the graph state to an empty dict."""
33+
self.state = {}
1634

1735
def add_node(self, node):
1836
if node.name in self.nodes:
@@ -100,17 +118,20 @@ async def execute(self, input):
100118
self.callback = self.get_callback()
101119
current_node = self.start_node
102120
state = input
121+
# Don't reset the graph state if it was initialized with initial_state
103122
steps = 0
104123
while True:
105124
# we invoke the node here
106125
if current_node.stream:
107126
# add callback tokens and param here if we are streaming
108127
await self.callback.start_node(node_name=current_node.name)
109-
output = await current_node.invoke(input=state, callback=self.callback)
128+
# Include graph's internal state in the node execution context
129+
output = await current_node.invoke(input=state, callback=self.callback, state=self.state)
110130
self._validate_output(output=output, node_name=current_node.name)
111131
await self.callback.end_node(node_name=current_node.name)
112132
else:
113-
output = await current_node.invoke(input=state)
133+
# Include graph's internal state in the node execution context
134+
output = await current_node.invoke(input=state, state=self.state)
114135
self._validate_output(output=output, node_name=current_node.name)
115136
# add output to state
116137
state = {**state, **output}
@@ -142,10 +163,21 @@ def get_callback(self):
142163
return self.callback
143164

144165
def _get_node_by_name(self, node_name: str) -> _Node:
145-
for node in self.nodes:
146-
if node.name == node_name:
147-
return node
148-
raise Exception(f"Node with name {node_name} not found.")
166+
"""Get a node by its name.
167+
168+
Args:
169+
node_name: The name of the node to find.
170+
171+
Returns:
172+
The node with the given name.
173+
174+
Raises:
175+
Exception: If no node with the given name is found.
176+
"""
177+
node = self.nodes.get(node_name)
178+
if node is None:
179+
raise Exception(f"Node with name {node_name} not found.")
180+
return node
149181

150182
def _get_next_node(self, current_node):
151183
for edge in self.edges:

graphai/nodes/base.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,14 +105,18 @@ def get_signature(cls):
105105
return "\n".join(signature_components)
106106

107107
@classmethod
108-
async def invoke(cls, input: Dict[str, Any], callback: Optional[Callback] = None):
108+
async def invoke(cls, input: Dict[str, Any], callback: Optional[Callback] = None, state: Optional[Dict[str, Any]] = None):
109109
if callback:
110110
if stream:
111111
input["callback"] = callback
112112
else:
113113
raise ValueError(
114114
f"Error in node {func.__name__}. When callback provided, stream must be True."
115115
)
116+
# Add state to the input if present and the parameter exists in the function signature
117+
if state is not None and "state" in cls._func_signature.parameters:
118+
input["state"] = state
119+
116120
instance = cls()
117121
out = await instance.execute(**input)
118122
return out

0 commit comments

Comments
 (0)