graphai-lib 0.0.2__tar.gz → 0.0.4__tar.gz

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.
@@ -0,0 +1,34 @@
1
+ Metadata-Version: 2.4
2
+ Name: graphai-lib
3
+ Version: 0.0.4
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
+ Provides-Extra: docs
21
+ Requires-Dist: pydoc-markdown>=4.8.2; python_version < "3.12" and extra == "docs"
22
+
23
+ # Philosophy
24
+
25
+ 1. Async-first
26
+ 2. Minimize abstractions
27
+ 3. One way to do one thing
28
+ 4. Graph-based AI
29
+
30
+ ## Installation
31
+
32
+ ```
33
+ pip install -qU graphai-lib
34
+ ```
@@ -3,4 +3,10 @@
3
3
  1. Async-first
4
4
  2. Minimize abstractions
5
5
  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
+ ```
@@ -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.append(node)
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
- # TODO add logic to check that source and destination are nodes
32
- # and they exist in the graph object already
33
- edge = Edge(source, destination)
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
- 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)
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
- 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)
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
- for node in self.nodes:
117
- if node.name == node_name:
118
- return node
119
- 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
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
- scale = min(0.8 / max_x, 0.8 / max_y)
179
- pos = {node: (x * scale, y * scale) for node, (x, y) in pos.items()}
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.")
@@ -31,6 +31,7 @@ class _Node:
31
31
  start: bool = False,
32
32
  end: bool = False,
33
33
  stream: bool = False,
34
+ name: str | None = None,
34
35
  ) -> Callable:
35
36
  """Decorator validating node structure.
36
37
  """
@@ -104,7 +105,7 @@ class _Node:
104
105
  return "\n".join(signature_components)
105
106
 
106
107
  @classmethod
107
- 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):
108
109
  if callback:
109
110
  if stream:
110
111
  input["callback"] = callback
@@ -112,12 +113,16 @@ class _Node:
112
113
  raise ValueError(
113
114
  f"Error in node {func.__name__}. When callback provided, stream must be True."
114
115
  )
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
+
115
120
  instance = cls()
116
121
  out = await instance.execute(**input)
117
122
  return out
118
123
 
119
124
  NodeClass.__name__ = func.__name__
120
- NodeClass.name = func.__name__
125
+ NodeClass.name = name or func.__name__
121
126
  NodeClass.__doc__ = func.__doc__
122
127
  NodeClass.is_start = start
123
128
  NodeClass.is_end = end
@@ -132,14 +137,15 @@ class _Node:
132
137
  start: bool = False,
133
138
  end: bool = False,
134
139
  stream: bool = False,
140
+ name: str | None = None,
135
141
  ):
136
142
  # We must wrap the call to the decorator in a function for it to work
137
143
  # 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)
144
+ def wrap(func: Callable, start=start, end=end, stream=stream, name=name) -> Callable:
145
+ return self._node(func=func, start=start, end=end, stream=stream, name=name)
140
146
  if func:
141
147
  # Decorator is called without parenthesis
142
- return wrap(func=func, start=start, end=end, stream=stream)
148
+ return wrap(func=func, start=start, end=end, stream=stream, name=name)
143
149
  # Decorator is called with parenthesis
144
150
  return wrap
145
151
 
@@ -0,0 +1,34 @@
1
+ Metadata-Version: 2.4
2
+ Name: graphai-lib
3
+ Version: 0.0.4
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
+ Provides-Extra: docs
21
+ Requires-Dist: pydoc-markdown>=4.8.2; python_version < "3.12" and extra == "docs"
22
+
23
+ # Philosophy
24
+
25
+ 1. Async-first
26
+ 2. Minimize abstractions
27
+ 3. One way to do one thing
28
+ 4. Graph-based AI
29
+
30
+ ## Installation
31
+
32
+ ```
33
+ pip install -qU graphai-lib
34
+ ```
@@ -0,0 +1,13 @@
1
+ README.md
2
+ pyproject.toml
3
+ graphai/__init__.py
4
+ graphai/callback.py
5
+ graphai/graph.py
6
+ graphai/utils.py
7
+ graphai/nodes/__init__.py
8
+ graphai/nodes/base.py
9
+ graphai_lib.egg-info/PKG-INFO
10
+ graphai_lib.egg-info/SOURCES.txt
11
+ graphai_lib.egg-info/dependency_links.txt
12
+ graphai_lib.egg-info/requires.txt
13
+ graphai_lib.egg-info/top_level.txt
@@ -0,0 +1,19 @@
1
+ semantic-router>=0.1.5
2
+ networkx>=3.4.2
3
+ matplotlib>=3.10.0
4
+
5
+ [dev]
6
+ ipykernel>=6.25.0
7
+ ruff>=0.1.5
8
+ pytest>=8.2.0
9
+ pytest-mock>=3.12.0
10
+ pytest-cov>=4.1.0
11
+ pytest-xdist>=3.5.0
12
+ pytest-asyncio>=0.24.0
13
+ mypy>=1.7.1
14
+ black[jupyter]<24.5.0,>=23.12.1
15
+
16
+ [docs]
17
+
18
+ [docs:python_version < "3.12"]
19
+ pydoc-markdown>=4.8.2
@@ -0,0 +1 @@
1
+ graphai
@@ -0,0 +1,32 @@
1
+ [project]
2
+ name = "graphai-lib"
3
+ version = "0.0.4"
4
+ description = "Not an AI framework"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10,<3.14"
7
+ dependencies = [
8
+ "semantic-router>=0.1.5",
9
+ "networkx>=3.4.2",
10
+ "matplotlib>=3.10.0",
11
+ ]
12
+
13
+ [project.optional-dependencies]
14
+ dev = [
15
+ "ipykernel>=6.25.0",
16
+ "ruff>=0.1.5",
17
+ "pytest>=8.2.0",
18
+ "pytest-mock>=3.12.0",
19
+ "pytest-cov>=4.1.0",
20
+ "pytest-xdist>=3.5.0",
21
+ "pytest-asyncio>=0.24.0",
22
+ "mypy>=1.7.1",
23
+ "black[jupyter]>=23.12.1,<24.5.0",
24
+ ]
25
+ docs = ["pydoc-markdown>=4.8.2 ; python_version < '3.12'"]
26
+
27
+ [build-system]
28
+ requires = ["setuptools>=61.0"]
29
+ build-backend = "setuptools.build_meta"
30
+
31
+ [tool.setuptools]
32
+ packages = ["graphai", "graphai.nodes"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,29 +0,0 @@
1
- [tool.poetry]
2
- name = "graphai-lib"
3
- version = "0.0.2"
4
- description = ""
5
- authors = ["Aurelio AI <hello@aurelio.ai>"]
6
- readme = "README.md"
7
- packages = [{include = "graphai"}]
8
- license = "MIT"
9
-
10
- [tool.poetry.dependencies]
11
- python = ">=3.10,<3.14"
12
- semantic-router = ">=0.1.0.dev4"
13
- networkx = "^3.4.2"
14
- matplotlib = "^3.10.0"
15
-
16
- [tool.poetry.group.dev.dependencies]
17
- ipykernel = "^6.25.0"
18
- ruff = "^0.1.5"
19
- pytest = "^8.2"
20
- pytest-mock = "^3.12.0"
21
- pytest-cov = "^4.1.0"
22
- pytest-xdist = "^3.5.0"
23
- pytest-asyncio = "^0.24.0"
24
- mypy = "^1.7.1"
25
- black = {extras = ["jupyter"], version = ">=23.12.1,<24.5.0"}
26
-
27
- [build-system]
28
- requires = ["poetry-core"]
29
- build-backend = "poetry.core.masonry.api"