graphai-lib 0.0.2__py3-none-any.whl → 0.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- graphai/graph.py +84 -22
- graphai_lib-0.0.3.dist-info/METADATA +32 -0
- graphai_lib-0.0.3.dist-info/RECORD +8 -0
- {graphai_lib-0.0.2.dist-info → graphai_lib-0.0.3.dist-info}/WHEEL +2 -1
- graphai_lib-0.0.3.dist-info/top_level.txt +1 -0
- graphai/nodes/__init__.py +0 -3
- graphai/nodes/base.py +0 -148
- graphai_lib-0.0.2.dist-info/METADATA +0 -25
- graphai_lib-0.0.2.dist-info/RECORD +0 -9
graphai/graph.py
CHANGED
@@ -1,21 +1,41 @@
|
|
1
|
-
from typing import List, Dict, Any
|
1
|
+
from typing import List, Dict, Any, Optional
|
2
2
|
from graphai.nodes.base import _Node
|
3
3
|
from graphai.callback import Callback
|
4
4
|
from semantic_router.utils.logger import logger
|
5
5
|
|
6
6
|
|
7
7
|
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] = []
|
13
13
|
self.Callback = Callback
|
14
14
|
self.callback = None
|
15
15
|
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 = {}
|
16
34
|
|
17
35
|
def add_node(self, node):
|
18
|
-
self.nodes
|
36
|
+
if node.name in self.nodes:
|
37
|
+
raise Exception(f"Node with name '{node.name}' already exists.")
|
38
|
+
self.nodes[node.name] = node
|
19
39
|
if node.is_start:
|
20
40
|
if self.start_node is not None:
|
21
41
|
raise Exception(
|
@@ -27,10 +47,37 @@ class Graph:
|
|
27
47
|
if node.is_end:
|
28
48
|
self.end_nodes.append(node)
|
29
49
|
|
30
|
-
def add_edge(self, source: _Node, destination: _Node):
|
31
|
-
|
32
|
-
|
33
|
-
|
50
|
+
def add_edge(self, source: _Node | str, destination: _Node | str):
|
51
|
+
"""Adds an edge between two nodes that already exist in the graph.
|
52
|
+
|
53
|
+
Args:
|
54
|
+
source: The source node or its name.
|
55
|
+
destination: The destination node or its name.
|
56
|
+
"""
|
57
|
+
source_node, destination_node = None, None
|
58
|
+
# get source node from graph
|
59
|
+
if isinstance(source, str):
|
60
|
+
source_node = self.nodes.get(source)
|
61
|
+
else:
|
62
|
+
# Check if it's a node-like object by looking for required attributes
|
63
|
+
if hasattr(source, 'name'):
|
64
|
+
source_node = self.nodes.get(source.name)
|
65
|
+
if source_node is None:
|
66
|
+
raise ValueError(
|
67
|
+
f"Node with name '{source.name if hasattr(source, 'name') else source}' not found."
|
68
|
+
)
|
69
|
+
# get destination node from graph
|
70
|
+
if isinstance(destination, str):
|
71
|
+
destination_node = self.nodes.get(destination)
|
72
|
+
else:
|
73
|
+
# Check if it's a node-like object by looking for required attributes
|
74
|
+
if hasattr(destination, 'name'):
|
75
|
+
destination_node = self.nodes.get(destination.name)
|
76
|
+
if destination_node is None:
|
77
|
+
raise ValueError(
|
78
|
+
f"Node with name '{destination.name if hasattr(destination, 'name') else destination}' not found."
|
79
|
+
)
|
80
|
+
edge = Edge(source_node, destination_node)
|
34
81
|
self.edges.append(edge)
|
35
82
|
|
36
83
|
def add_router(self, sources: list[_Node], router: _Node, destinations: List[_Node]):
|
@@ -71,17 +118,20 @@ class Graph:
|
|
71
118
|
self.callback = self.get_callback()
|
72
119
|
current_node = self.start_node
|
73
120
|
state = input
|
121
|
+
# Don't reset the graph state if it was initialized with initial_state
|
74
122
|
steps = 0
|
75
123
|
while True:
|
76
124
|
# we invoke the node here
|
77
125
|
if current_node.stream:
|
78
126
|
# add callback tokens and param here if we are streaming
|
79
127
|
await self.callback.start_node(node_name=current_node.name)
|
80
|
-
|
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)
|
81
130
|
self._validate_output(output=output, node_name=current_node.name)
|
82
131
|
await self.callback.end_node(node_name=current_node.name)
|
83
132
|
else:
|
84
|
-
|
133
|
+
# Include graph's internal state in the node execution context
|
134
|
+
output = await current_node.invoke(input=state, state=self.state)
|
85
135
|
self._validate_output(output=output, node_name=current_node.name)
|
86
136
|
# add output to state
|
87
137
|
state = {**state, **output}
|
@@ -113,10 +163,21 @@ class Graph:
|
|
113
163
|
return self.callback
|
114
164
|
|
115
165
|
def _get_node_by_name(self, node_name: str) -> _Node:
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
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
|
120
181
|
|
121
182
|
def _get_next_node(self, current_node):
|
122
183
|
for edge in self.edges:
|
@@ -139,7 +200,7 @@ class Graph:
|
|
139
200
|
|
140
201
|
G = nx.DiGraph()
|
141
202
|
|
142
|
-
for node in self.nodes:
|
203
|
+
for node in self.nodes.values():
|
143
204
|
G.add_node(node.name)
|
144
205
|
|
145
206
|
for edge in self.edges:
|
@@ -173,10 +234,11 @@ class Graph:
|
|
173
234
|
pos[node] = (pos[node][0] - x_center, pos[node][1])
|
174
235
|
|
175
236
|
# Scale the layout
|
176
|
-
max_x = max(abs(p[0]) for p in pos.values())
|
177
|
-
max_y = max(abs(p[1]) for p in pos.values())
|
178
|
-
|
179
|
-
|
237
|
+
max_x = max(abs(p[0]) for p in pos.values()) if pos else 1
|
238
|
+
max_y = max(abs(p[1]) for p in pos.values()) if pos else 1
|
239
|
+
if max_x > 0 and max_y > 0:
|
240
|
+
scale = min(0.8 / max_x, 0.8 / max_y)
|
241
|
+
pos = {node: (x * scale, y * scale) for node, (x, y) in pos.items()}
|
180
242
|
|
181
243
|
else:
|
182
244
|
print("Warning: The graph contains cycles. Visualization will use a spring layout.")
|
@@ -0,0 +1,32 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: graphai-lib
|
3
|
+
Version: 0.0.3
|
4
|
+
Summary: Not an AI framework
|
5
|
+
Requires-Python: <3.14,>=3.10
|
6
|
+
Description-Content-Type: text/markdown
|
7
|
+
Requires-Dist: semantic-router>=0.1.5
|
8
|
+
Requires-Dist: networkx>=3.4.2
|
9
|
+
Requires-Dist: matplotlib>=3.10.0
|
10
|
+
Provides-Extra: dev
|
11
|
+
Requires-Dist: ipykernel>=6.25.0; extra == "dev"
|
12
|
+
Requires-Dist: ruff>=0.1.5; extra == "dev"
|
13
|
+
Requires-Dist: pytest>=8.2.0; extra == "dev"
|
14
|
+
Requires-Dist: pytest-mock>=3.12.0; extra == "dev"
|
15
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
|
16
|
+
Requires-Dist: pytest-xdist>=3.5.0; extra == "dev"
|
17
|
+
Requires-Dist: pytest-asyncio>=0.24.0; extra == "dev"
|
18
|
+
Requires-Dist: mypy>=1.7.1; extra == "dev"
|
19
|
+
Requires-Dist: black[jupyter]<24.5.0,>=23.12.1; extra == "dev"
|
20
|
+
|
21
|
+
# Philosophy
|
22
|
+
|
23
|
+
1. Async-first
|
24
|
+
2. Minimize abstractions
|
25
|
+
3. One way to do one thing
|
26
|
+
4. Graph-based AI
|
27
|
+
|
28
|
+
## Installation
|
29
|
+
|
30
|
+
```
|
31
|
+
pip install -qU graphai-lib
|
32
|
+
```
|
@@ -0,0 +1,8 @@
|
|
1
|
+
graphai/__init__.py,sha256=EHigFOWewDXLZXbdfjZH9kdLPhw6NT0ChS77lNAVAA8,109
|
2
|
+
graphai/callback.py,sha256=K-h44pyL2VLXwJzIB_bcVYp5R6xv8zNca5FmN6994Uk,7598
|
3
|
+
graphai/graph.py,sha256=EALHEhbXAaJmTvm7cL3Tdh0moRIw7lIyJDCNnCts2QA,10335
|
4
|
+
graphai/utils.py,sha256=zrgpk82rIn7lwh631KhN-OgMAJMdbm0k5GPL1eMf2sQ,4522
|
5
|
+
graphai_lib-0.0.3.dist-info/METADATA,sha256=fOjGeptDi6YD-fYZD9NtFv4_lzLWjMtvBnfwoQ9zLfo,879
|
6
|
+
graphai_lib-0.0.3.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
|
7
|
+
graphai_lib-0.0.3.dist-info/top_level.txt,sha256=TXlqmhLViX-3xGH2g5w6cavRd-QMf229Hl88jdMOGt8,8
|
8
|
+
graphai_lib-0.0.3.dist-info/RECORD,,
|
@@ -0,0 +1 @@
|
|
1
|
+
graphai
|
graphai/nodes/__init__.py
DELETED
graphai/nodes/base.py
DELETED
@@ -1,148 +0,0 @@
|
|
1
|
-
import inspect
|
2
|
-
from typing import Any, Callable, Dict, Optional
|
3
|
-
|
4
|
-
from graphai.callback import Callback
|
5
|
-
from graphai.utils import FunctionSchema
|
6
|
-
|
7
|
-
|
8
|
-
class NodeMeta(type):
|
9
|
-
@staticmethod
|
10
|
-
def positional_to_kwargs(cls_type, args) -> Dict[str, Any]:
|
11
|
-
init_signature = inspect.signature(cls_type.__init__)
|
12
|
-
init_params = {name: arg for name, arg in init_signature.parameters.items() if name != "self"}
|
13
|
-
return init_params
|
14
|
-
|
15
|
-
def __call__(cls, *args, **kwargs):
|
16
|
-
named_positional_args = NodeMeta.positional_to_kwargs(cls, args)
|
17
|
-
kwargs.update(named_positional_args)
|
18
|
-
return super().__call__(**kwargs)
|
19
|
-
|
20
|
-
|
21
|
-
class _Node:
|
22
|
-
def __init__(
|
23
|
-
self,
|
24
|
-
is_router: bool = False,
|
25
|
-
):
|
26
|
-
self.is_router = is_router
|
27
|
-
|
28
|
-
def _node(
|
29
|
-
self,
|
30
|
-
func: Callable,
|
31
|
-
start: bool = False,
|
32
|
-
end: bool = False,
|
33
|
-
stream: bool = False,
|
34
|
-
) -> Callable:
|
35
|
-
"""Decorator validating node structure.
|
36
|
-
"""
|
37
|
-
if not callable(func):
|
38
|
-
raise ValueError("Node must be a callable function.")
|
39
|
-
|
40
|
-
func_signature = inspect.signature(func)
|
41
|
-
schema = FunctionSchema(func)
|
42
|
-
|
43
|
-
class NodeClass:
|
44
|
-
_func_signature = func_signature
|
45
|
-
is_router = None
|
46
|
-
_stream = stream
|
47
|
-
|
48
|
-
def __init__(self):
|
49
|
-
self._expected_params = set(self._func_signature.parameters.keys())
|
50
|
-
|
51
|
-
async def execute(self, *args, **kwargs):
|
52
|
-
# Prepare arguments, including callback if stream is True
|
53
|
-
params_dict = await self._parse_params(*args, **kwargs)
|
54
|
-
return await func(**params_dict) # Pass only the necessary arguments
|
55
|
-
|
56
|
-
async def _parse_params(self, *args, **kwargs) -> Dict[str, Any]:
|
57
|
-
# filter out unexpected keyword args
|
58
|
-
expected_kwargs = {k: v for k, v in kwargs.items() if k in self._expected_params}
|
59
|
-
# Convert args to kwargs based on the function signature
|
60
|
-
args_names = list(self._func_signature.parameters.keys())[1:len(args)+1] # skip 'self'
|
61
|
-
expected_args_kwargs = dict(zip(args_names, args))
|
62
|
-
# Combine filtered args and kwargs
|
63
|
-
combined_params = {**expected_args_kwargs, **expected_kwargs}
|
64
|
-
|
65
|
-
# Bind the current instance attributes to the function signature
|
66
|
-
if "callback" in self._expected_params and not stream:
|
67
|
-
raise ValueError(
|
68
|
-
f"Node {func.__name__}: requires stream=True when callback is defined."
|
69
|
-
)
|
70
|
-
bound_params = self._func_signature.bind_partial(**combined_params)
|
71
|
-
# get the default parameters (if any)
|
72
|
-
bound_params.apply_defaults()
|
73
|
-
params_dict = bound_params.arguments.copy()
|
74
|
-
# Filter arguments to match the next node's parameters
|
75
|
-
filtered_params = {
|
76
|
-
k: v for k, v in params_dict.items() if k in self._expected_params
|
77
|
-
}
|
78
|
-
# confirm all required parameters are present
|
79
|
-
missing_params = [
|
80
|
-
p for p in self._expected_params if p not in filtered_params
|
81
|
-
]
|
82
|
-
# if anything is missing we raise an error
|
83
|
-
if missing_params:
|
84
|
-
raise ValueError(
|
85
|
-
f"Missing required parameters for the {func.__name__} node: {', '.join(missing_params)}"
|
86
|
-
)
|
87
|
-
return filtered_params
|
88
|
-
|
89
|
-
|
90
|
-
@classmethod
|
91
|
-
def get_signature(cls):
|
92
|
-
"""Returns the signature of the decorated function as LLM readable
|
93
|
-
string.
|
94
|
-
"""
|
95
|
-
signature_components = []
|
96
|
-
if NodeClass._func_signature:
|
97
|
-
for param in NodeClass._func_signature.parameters.values():
|
98
|
-
if param.default is param.empty:
|
99
|
-
signature_components.append(f"{param.name}: {param.annotation}")
|
100
|
-
else:
|
101
|
-
signature_components.append(f"{param.name}: {param.annotation} = {param.default}")
|
102
|
-
else:
|
103
|
-
return "No signature"
|
104
|
-
return "\n".join(signature_components)
|
105
|
-
|
106
|
-
@classmethod
|
107
|
-
async def invoke(cls, input: Dict[str, Any], callback: Optional[Callback] = None):
|
108
|
-
if callback:
|
109
|
-
if stream:
|
110
|
-
input["callback"] = callback
|
111
|
-
else:
|
112
|
-
raise ValueError(
|
113
|
-
f"Error in node {func.__name__}. When callback provided, stream must be True."
|
114
|
-
)
|
115
|
-
instance = cls()
|
116
|
-
out = await instance.execute(**input)
|
117
|
-
return out
|
118
|
-
|
119
|
-
NodeClass.__name__ = func.__name__
|
120
|
-
NodeClass.name = func.__name__
|
121
|
-
NodeClass.__doc__ = func.__doc__
|
122
|
-
NodeClass.is_start = start
|
123
|
-
NodeClass.is_end = end
|
124
|
-
NodeClass.is_router = self.is_router
|
125
|
-
NodeClass.stream = stream
|
126
|
-
NodeClass.schema = schema
|
127
|
-
return NodeClass
|
128
|
-
|
129
|
-
def __call__(
|
130
|
-
self,
|
131
|
-
func: Optional[Callable] = None,
|
132
|
-
start: bool = False,
|
133
|
-
end: bool = False,
|
134
|
-
stream: bool = False,
|
135
|
-
):
|
136
|
-
# We must wrap the call to the decorator in a function for it to work
|
137
|
-
# correctly with or without parenthesis
|
138
|
-
def wrap(func: Callable, start=start, end=end, stream=stream) -> Callable:
|
139
|
-
return self._node(func=func, start=start, end=end, stream=stream)
|
140
|
-
if func:
|
141
|
-
# Decorator is called without parenthesis
|
142
|
-
return wrap(func=func, start=start, end=end, stream=stream)
|
143
|
-
# Decorator is called with parenthesis
|
144
|
-
return wrap
|
145
|
-
|
146
|
-
|
147
|
-
node = _Node()
|
148
|
-
router = _Node(is_router=True)
|
@@ -1,25 +0,0 @@
|
|
1
|
-
Metadata-Version: 2.1
|
2
|
-
Name: graphai-lib
|
3
|
-
Version: 0.0.2
|
4
|
-
Summary:
|
5
|
-
License: MIT
|
6
|
-
Author: Aurelio AI
|
7
|
-
Author-email: hello@aurelio.ai
|
8
|
-
Requires-Python: >=3.10,<3.14
|
9
|
-
Classifier: License :: OSI Approved :: MIT License
|
10
|
-
Classifier: Programming Language :: Python :: 3
|
11
|
-
Classifier: Programming Language :: Python :: 3.10
|
12
|
-
Classifier: Programming Language :: Python :: 3.11
|
13
|
-
Classifier: Programming Language :: Python :: 3.12
|
14
|
-
Classifier: Programming Language :: Python :: 3.13
|
15
|
-
Requires-Dist: matplotlib (>=3.10.0,<4.0.0)
|
16
|
-
Requires-Dist: networkx (>=3.4.2,<4.0.0)
|
17
|
-
Requires-Dist: semantic-router (>=0.1.0.dev4)
|
18
|
-
Description-Content-Type: text/markdown
|
19
|
-
|
20
|
-
# Philosophy
|
21
|
-
|
22
|
-
1. Async-first
|
23
|
-
2. Minimize abstractions
|
24
|
-
3. One way to do one thing
|
25
|
-
4. Graph-based AI
|
@@ -1,9 +0,0 @@
|
|
1
|
-
graphai/__init__.py,sha256=EHigFOWewDXLZXbdfjZH9kdLPhw6NT0ChS77lNAVAA8,109
|
2
|
-
graphai/callback.py,sha256=K-h44pyL2VLXwJzIB_bcVYp5R6xv8zNca5FmN6994Uk,7598
|
3
|
-
graphai/graph.py,sha256=Gn_bl0iAtcH9QT54G2WHJDbxv0TPkVwuPOgAfu4bHT4,7687
|
4
|
-
graphai/nodes/__init__.py,sha256=4826Ubk5yUfbVH7F8DmoTKQyax624Q2QJHsGxqgQ_ng,73
|
5
|
-
graphai/nodes/base.py,sha256=VHceReX-FcRTrq8eWF6vfDyRfc4F8Zdcya5UXSPFbmw,5937
|
6
|
-
graphai/utils.py,sha256=zrgpk82rIn7lwh631KhN-OgMAJMdbm0k5GPL1eMf2sQ,4522
|
7
|
-
graphai_lib-0.0.2.dist-info/METADATA,sha256=Asgg3vqJWJRAq0VmLnzUC8dsfUCcKoH4D1pLSUopUVg,732
|
8
|
-
graphai_lib-0.0.2.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
9
|
-
graphai_lib-0.0.2.dist-info/RECORD,,
|