graphai-lib 0.0.3__py3-none-any.whl → 0.0.4__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.
@@ -0,0 +1,3 @@
1
+ from graphai.nodes.base import node, router
2
+
3
+ __all__ = ["node", "router"]
graphai/nodes/base.py ADDED
@@ -0,0 +1,154 @@
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
+ name: str | None = None,
35
+ ) -> Callable:
36
+ """Decorator validating node structure.
37
+ """
38
+ if not callable(func):
39
+ raise ValueError("Node must be a callable function.")
40
+
41
+ func_signature = inspect.signature(func)
42
+ schema = FunctionSchema(func)
43
+
44
+ class NodeClass:
45
+ _func_signature = func_signature
46
+ is_router = None
47
+ _stream = stream
48
+
49
+ def __init__(self):
50
+ self._expected_params = set(self._func_signature.parameters.keys())
51
+
52
+ async def execute(self, *args, **kwargs):
53
+ # Prepare arguments, including callback if stream is True
54
+ params_dict = await self._parse_params(*args, **kwargs)
55
+ return await func(**params_dict) # Pass only the necessary arguments
56
+
57
+ async def _parse_params(self, *args, **kwargs) -> Dict[str, Any]:
58
+ # filter out unexpected keyword args
59
+ expected_kwargs = {k: v for k, v in kwargs.items() if k in self._expected_params}
60
+ # Convert args to kwargs based on the function signature
61
+ args_names = list(self._func_signature.parameters.keys())[1:len(args)+1] # skip 'self'
62
+ expected_args_kwargs = dict(zip(args_names, args))
63
+ # Combine filtered args and kwargs
64
+ combined_params = {**expected_args_kwargs, **expected_kwargs}
65
+
66
+ # Bind the current instance attributes to the function signature
67
+ if "callback" in self._expected_params and not stream:
68
+ raise ValueError(
69
+ f"Node {func.__name__}: requires stream=True when callback is defined."
70
+ )
71
+ bound_params = self._func_signature.bind_partial(**combined_params)
72
+ # get the default parameters (if any)
73
+ bound_params.apply_defaults()
74
+ params_dict = bound_params.arguments.copy()
75
+ # Filter arguments to match the next node's parameters
76
+ filtered_params = {
77
+ k: v for k, v in params_dict.items() if k in self._expected_params
78
+ }
79
+ # confirm all required parameters are present
80
+ missing_params = [
81
+ p for p in self._expected_params if p not in filtered_params
82
+ ]
83
+ # if anything is missing we raise an error
84
+ if missing_params:
85
+ raise ValueError(
86
+ f"Missing required parameters for the {func.__name__} node: {', '.join(missing_params)}"
87
+ )
88
+ return filtered_params
89
+
90
+
91
+ @classmethod
92
+ def get_signature(cls):
93
+ """Returns the signature of the decorated function as LLM readable
94
+ string.
95
+ """
96
+ signature_components = []
97
+ if NodeClass._func_signature:
98
+ for param in NodeClass._func_signature.parameters.values():
99
+ if param.default is param.empty:
100
+ signature_components.append(f"{param.name}: {param.annotation}")
101
+ else:
102
+ signature_components.append(f"{param.name}: {param.annotation} = {param.default}")
103
+ else:
104
+ return "No signature"
105
+ return "\n".join(signature_components)
106
+
107
+ @classmethod
108
+ async def invoke(cls, input: Dict[str, Any], callback: Optional[Callback] = None, state: Optional[Dict[str, Any]] = None):
109
+ if callback:
110
+ if stream:
111
+ input["callback"] = callback
112
+ else:
113
+ raise ValueError(
114
+ f"Error in node {func.__name__}. When callback provided, stream must be True."
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
+
120
+ instance = cls()
121
+ out = await instance.execute(**input)
122
+ return out
123
+
124
+ NodeClass.__name__ = func.__name__
125
+ NodeClass.name = name or func.__name__
126
+ NodeClass.__doc__ = func.__doc__
127
+ NodeClass.is_start = start
128
+ NodeClass.is_end = end
129
+ NodeClass.is_router = self.is_router
130
+ NodeClass.stream = stream
131
+ NodeClass.schema = schema
132
+ return NodeClass
133
+
134
+ def __call__(
135
+ self,
136
+ func: Optional[Callable] = None,
137
+ start: bool = False,
138
+ end: bool = False,
139
+ stream: bool = False,
140
+ name: str | None = None,
141
+ ):
142
+ # We must wrap the call to the decorator in a function for it to work
143
+ # correctly with or without parenthesis
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)
146
+ if func:
147
+ # Decorator is called without parenthesis
148
+ return wrap(func=func, start=start, end=end, stream=stream, name=name)
149
+ # Decorator is called with parenthesis
150
+ return wrap
151
+
152
+
153
+ node = _Node()
154
+ router = _Node(is_router=True)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphai-lib
3
- Version: 0.0.3
3
+ Version: 0.0.4
4
4
  Summary: Not an AI framework
5
5
  Requires-Python: <3.14,>=3.10
6
6
  Description-Content-Type: text/markdown
@@ -17,6 +17,8 @@ Requires-Dist: pytest-xdist>=3.5.0; extra == "dev"
17
17
  Requires-Dist: pytest-asyncio>=0.24.0; extra == "dev"
18
18
  Requires-Dist: mypy>=1.7.1; extra == "dev"
19
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"
20
22
 
21
23
  # Philosophy
22
24
 
@@ -0,0 +1,10 @@
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/nodes/__init__.py,sha256=4826Ubk5yUfbVH7F8DmoTKQyax624Q2QJHsGxqgQ_ng,73
6
+ graphai/nodes/base.py,sha256=SZdYhFfXdtFmabFbMRcEGd8_h8w-g6s4I7hMEo6JCk8,6331
7
+ graphai_lib-0.0.4.dist-info/METADATA,sha256=wfxK82ZO-slBl-xVNmdbYGVvPo7YKm8-SLfdrkNWtKs,982
8
+ graphai_lib-0.0.4.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
9
+ graphai_lib-0.0.4.dist-info/top_level.txt,sha256=TXlqmhLViX-3xGH2g5w6cavRd-QMf229Hl88jdMOGt8,8
10
+ graphai_lib-0.0.4.dist-info/RECORD,,
@@ -1,8 +0,0 @@
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,,