chainix 0.1.0__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.
chainix/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .main import AsyncChainClient
2
+
3
+ __version__ = "0.1.1"
4
+ __all__ = ["AsyncChainClient"]
chainix/main.py ADDED
@@ -0,0 +1,223 @@
1
+ import requests
2
+ import time
3
+ from typing import Dict, Callable, Any
4
+
5
+
6
+ class AsyncChainClient:
7
+ """
8
+ Client for executing asynchronous chains with custom function callbacks.
9
+ """
10
+
11
+ def __init__(
12
+ self,
13
+ chain_id: str,
14
+ api_key: str,
15
+ base_url: str = "https://chainix.ai",
16
+ max_wait_time: int = 300,
17
+ poll_interval: int = 5,
18
+ verbose: bool = True
19
+ ):
20
+ """
21
+ Initialize the AsyncChainClient.
22
+
23
+ Args:
24
+ chain_id: Unique identifier for your chain
25
+ api_key: API key for authentication
26
+ base_url: Base URL of the chain server
27
+ max_wait_time: Maximum time to wait for chain completion in seconds
28
+ poll_interval: Time between status polls in seconds
29
+ verbose: Whether to print status messages (default True)
30
+ """
31
+ self.chain_id = chain_id
32
+ self.api_key = api_key
33
+ self.base_url = base_url.rstrip('/')
34
+ self.max_wait_time = max_wait_time
35
+ self.poll_interval = max(poll_interval, 3)
36
+ self.functions: Dict[str, Callable] = {}
37
+ self.verbose = verbose
38
+
39
+ # Set up default headers
40
+ self.headers = {
41
+ 'X-API-Key': self.api_key,
42
+ 'Content-Type': 'application/json'
43
+ }
44
+
45
+ def _log(self, message: str):
46
+ """Print message if verbose mode is enabled."""
47
+ if self.verbose:
48
+ print(message)
49
+
50
+
51
+ def register_function(self, function_id: str, function: Callable[[Dict[str, Any]], Any]) -> None:
52
+ """
53
+ Register a function that can be called during chain execution.
54
+
55
+ Args:
56
+ function_id: Unique identifier for the function
57
+ function: Callable that takes a dictionary of arguments and returns a result
58
+ """
59
+ self.functions[function_id] = function
60
+
61
+ def register_functions(self, functions: Dict[str, Callable[[Dict[str, Any]], Any]]) -> None:
62
+ """
63
+ Register multiple functions at once.
64
+
65
+ Args:
66
+ functions: Dictionary mapping function IDs to callable functions
67
+ """
68
+ self.functions.update(functions)
69
+
70
+ def poll_chain_status(self, task_id: str) -> Dict[str, Any]:
71
+ """
72
+ Poll the server for chain status until completion or timeout.
73
+ """
74
+ start_time = time.time()
75
+ status_url = f'{self.base_url}/api/chain-status/{self.chain_id}/{task_id}'
76
+
77
+ while time.time() - start_time < self.max_wait_time:
78
+ try:
79
+ status_response = requests.get(status_url, headers=self.headers)
80
+
81
+ if not status_response.ok:
82
+ try:
83
+ error_data = status_response.json()
84
+ if error_data.get('detail'):
85
+ self._log(f"Error polling chain status: {error_data['detail']}")
86
+ else:
87
+ self._log(f"Error response: {error_data}")
88
+ except:
89
+ self._log(f"Error {status_response.status_code}: {status_response.text}")
90
+ time.sleep(self.poll_interval)
91
+ continue
92
+
93
+ status_data = status_response.json()
94
+ self._log(f"Async status: {status_data.get('status', 'unknown')}")
95
+
96
+ if status_data.get('status') == 'complete':
97
+ return status_data.get('result', {})
98
+ elif status_data.get('status') == 'failed':
99
+ raise Exception(f"Chain failed: {status_data.get('result')}")
100
+
101
+ # Chain is still running, wait
102
+ time.sleep(self.poll_interval)
103
+ except requests.RequestException as e:
104
+ self._log(f"Error polling chain status: {e}")
105
+ time.sleep(self.poll_interval)
106
+
107
+ raise TimeoutError(f"Chain with run id {task_id} did not complete within {self.max_wait_time} seconds.")
108
+
109
+ def run_chain(
110
+ self,
111
+ initial_variables: Dict[str, Any],
112
+ test: bool = False,
113
+ ) -> Dict[str, Any]:
114
+ """
115
+ Execute an asynchronous chain with the provided initial_variables.
116
+
117
+ Args:
118
+ initial_variables: Dictionary of initial variables for the chain
119
+ test: Whether this is a test run
120
+ vars_to_update: Optional dictionary of variables to update during execution
121
+
122
+ Returns:
123
+ Dictionary containing the final chain result
124
+ """
125
+ url = f'{self.base_url}/api/run-chain/{self.chain_id}'
126
+
127
+ payload = {
128
+ 'initial_variables': initial_variables,
129
+ 'initial_run': True,
130
+ 'test': test,
131
+ }
132
+
133
+ vars_to_update = {}
134
+
135
+ if vars_to_update:
136
+ payload['vars_to_update'] = vars_to_update
137
+
138
+ response = requests.post(url, json=payload, headers=self.headers)
139
+ if not response.ok:
140
+ try:
141
+ error_data = response.json()
142
+ if error_data.get('detail'):
143
+ self._log(f"Error: {error_data['detail']}")
144
+ self._log(f"Response: {error_data}")
145
+ else:
146
+ self._log(f"Response: {error_data}")
147
+ except:
148
+ self._log(f"Error {response.status_code}: {response.text}")
149
+ return {}
150
+
151
+ initial_response = response.json()
152
+ task_id = initial_response.get('data', {}).get('taskId')
153
+ if not task_id:
154
+ raise Exception("No task ID received from initial request.")
155
+
156
+ self._log(f"Chain started with task ID: {task_id}")
157
+ response = self.poll_chain_status(task_id)
158
+
159
+ while not response.get('complete', False):
160
+ # extract the run ID
161
+ run_id = response.get('run_id')
162
+ # the run is asking for a function call
163
+ func_info = response.get('function')
164
+ if not func_info:
165
+ raise Exception("No function call information found in the response.")
166
+
167
+ returned_function_id = func_info.get('id')
168
+ arguments = func_info.get('args')
169
+
170
+ if returned_function_id not in self.functions:
171
+ raise Exception(f"Function '{returned_function_id}' not implemented.")
172
+
173
+ function_to_call = self.functions[returned_function_id]
174
+ try:
175
+ result = function_to_call(arguments)
176
+ except Exception as e:
177
+ self._log(f"Function '{returned_function_id}' failed: {e}")
178
+ success = False
179
+ function_vars_to_update = {}
180
+
181
+ else:
182
+ if not isinstance(result, dict):
183
+ raise Exception(f"Function '{returned_function_id}' must return a dictionary, got {type(result)}")
184
+
185
+ if 'success' not in result:
186
+ raise Exception(f"Function '{returned_function_id}' must return a dictionary with 'success' key to indicate if the function ran successfully.")
187
+
188
+ if 'vars_to_update' not in result:
189
+ raise Exception(f"Function '{returned_function_id}' must return a dictionary with 'vars_to_update' key. If there are no variables to update it should return an empty dictionary.")
190
+
191
+ success = result['success']
192
+ function_vars_to_update = result['vars_to_update']
193
+
194
+
195
+ payload = {
196
+ 'run_id': run_id,
197
+ 'success': success,
198
+ 'initial_run': False,
199
+ 'vars_to_update': function_vars_to_update
200
+ }
201
+
202
+ submit_resp = requests.post(url, json=payload, headers=self.headers)
203
+ if not submit_resp.ok:
204
+ try:
205
+ error_data = submit_resp.json()
206
+ if error_data.get('detail'):
207
+ self._log(f"Error submitting function result: {error_data['detail']}")
208
+ else:
209
+ self._log(f"Error response: {error_data}")
210
+ except:
211
+ self._log(f"Error {submit_resp.status_code}: {submit_resp.text}")
212
+ return {}
213
+
214
+ submit_response = submit_resp.json()
215
+ new_task_id = submit_response.get('data', {}).get('taskId')
216
+ if new_task_id:
217
+ self._log(f"Chain continued with task ID: {new_task_id}")
218
+ response = self.poll_chain_status(new_task_id)
219
+ else:
220
+ response = submit_response
221
+
222
+ self._log("Chain complete!")
223
+ return response
@@ -0,0 +1,311 @@
1
+ Metadata-Version: 2.4
2
+ Name: chainix
3
+ Version: 0.1.0
4
+ Summary: A client library for executing asynchronous chains with custom function callbacks
5
+ Home-page: https://github.com/ChainixDev/chainix-python
6
+ Author: Chainix
7
+ Author-email: jack@chainix.ai
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.7
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: requests>=2.25.0
14
+ Dynamic: author
15
+ Dynamic: author-email
16
+ Dynamic: classifier
17
+ Dynamic: description
18
+ Dynamic: description-content-type
19
+ Dynamic: home-page
20
+ Dynamic: requires-dist
21
+ Dynamic: requires-python
22
+ Dynamic: summary
23
+
24
+ # Chainix
25
+
26
+ A Python client library for executing chains with chainix.ai
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ pip install chainix
32
+ ```
33
+
34
+ ## Quick Start
35
+
36
+ ```python
37
+ from chainix import AsyncChainClient
38
+
39
+ # Initialize the client
40
+ client = AsyncChainClient(
41
+ chain_id="your-chains-id-here",
42
+ api_key="your-api-key-here",
43
+ )
44
+
45
+ # Define your custom functions
46
+ def refund(args):
47
+ try:
48
+ order_id = args['order id']
49
+ print(f"Issuing a refund for order: {order_id}")
50
+
51
+ # Your business logic here
52
+ # ... process refund ...
53
+ amount = 500
54
+
55
+ return {
56
+ 'success': True,
57
+ 'vars_to_update': {
58
+ 'refund_amount': amount
59
+ }
60
+ }
61
+ except Exception as e:
62
+ print(f"Failed to process refund: {e}")
63
+ return {
64
+ 'success': False,
65
+ 'vars_to_update': {}
66
+ }
67
+
68
+
69
+ def cancel_order(args):
70
+ try:
71
+ order_id = args['order id']
72
+ print(f"Cancelling order: {order_id}")
73
+
74
+ # Your business logic here
75
+ # ... perform cancellation ...
76
+
77
+ return {
78
+ 'success': True,
79
+ 'vars_to_update': {}
80
+ }
81
+ except Exception as e:
82
+ print(f"Failed to cancel order: {e}")
83
+ return {
84
+ 'success': False,
85
+ 'vars_to_update': {}
86
+ }
87
+
88
+
89
+ # Register your functions (use the actual function IDs from the chain on chainix.ai)
90
+ client.register_function("your-cancel-function-id", cancel_order)
91
+ client.register_function("your-refund-function-id", refund)
92
+
93
+ # Or, bulk register your functions
94
+ functions = {
95
+ "your-cancel-function-id": cancel_order,
96
+ "your-refund-function-id": refund,
97
+ }
98
+ client.register_functions(functions)
99
+
100
+ # Execute a chain
101
+ # Provide all initial variables needed to start the chain
102
+ result = client.run_chain({
103
+ 'message body': 'Hi, please cancel my order',
104
+ 'order id': '33433',
105
+ })
106
+
107
+ print("Chain completed:", result)
108
+ ```
109
+
110
+ ## Function Requirements
111
+
112
+ All registered functions **must** follow these requirements:
113
+
114
+ ### Function Signature
115
+ Your functions should accept a single dictionary argument containing the inputs for that function call step:
116
+
117
+ ```python
118
+ def my_function(args: dict) -> dict:
119
+ # Your business logic here
120
+ pass
121
+ ```
122
+
123
+ **How it works:**
124
+ 1. You define variables in your chain on chainix.ai (e.g., `order id`, `user email`, `action`)
125
+ 2. You create function call steps in your chain and specify which variables should be passed as inputs to each step
126
+ 3. When the chain reaches a function call step, it stops and calls your registered function via it's id
127
+ 4. Your function receives a dictionary where each key is a variable you specified as an input for that step, and each value is the current value of that variable in the chain
128
+
129
+ **Example:** If you have a function call step with `order id` and `user email` as inputs, your function will receive:
130
+ ```python
131
+ {
132
+ 'order id': '12345',
133
+ 'user email': 'user@example.com'
134
+ }
135
+ ```
136
+
137
+ ### Return Value
138
+ Your functions **must** return a dictionary with exactly two keys:
139
+
140
+ ```python
141
+ {
142
+ 'success': bool, # True if function executed successfully, False otherwise
143
+ 'vars_to_update': dict # Dictionary of variables to update in the chain (can be empty)
144
+ }
145
+ ```
146
+
147
+ **Important**: The keys in `vars_to_update` must exactly match the variable names you defined in your chain on chainix.ai. Only variables that exist in your chain can be updated. If you try to update a variable that doesn't exist in your chain, the chain will fail.
148
+
149
+ ### Example Function Structure
150
+
151
+ ```python
152
+ def process_order(args):
153
+ try:
154
+ # Extract arguments
155
+ order_id = args['order id']
156
+ action = args.get('action', 'process')
157
+
158
+ # Your business logic here
159
+ if action == 'cancel':
160
+ # ... cancellation logic ...
161
+ return {
162
+ 'success': True,
163
+ 'vars_to_update': {
164
+ 'order status': 'cancelled', # Must match variable name in your chain
165
+ 'cancellation date': '2024-01-01' # Must match variable name in your chain
166
+ }
167
+ }
168
+ elif action == 'fulfill':
169
+ # ... fulfillment logic ...
170
+ return {
171
+ 'success': True,
172
+ 'vars_to_update': {
173
+ 'order status': 'fulfilled', # Must match variable name in your chain
174
+ 'fulfillment date': '2024-01-01' # Must match variable name in your chain
175
+ }
176
+ }
177
+ else:
178
+ return {
179
+ 'success': False,
180
+ 'vars_to_update': {}
181
+ }
182
+
183
+ except Exception as e:
184
+ print(f"Error processing order: {e}")
185
+ return {
186
+ 'success': False,
187
+ 'vars_to_update': {}
188
+ }
189
+ ```
190
+
191
+ ## Function Registration
192
+
193
+ You can register functions individually or in bulk:
194
+
195
+ ```python
196
+ # Individual registration
197
+ client.register_function("function-id-1", my_function)
198
+
199
+ # Bulk registration
200
+ functions = {
201
+ "function-id-1": cancel_order,
202
+ "function-id-2": refund,
203
+ "function-id-3": process_order,
204
+ }
205
+ client.register_functions(functions)
206
+ ```
207
+
208
+ ## Configuration
209
+
210
+ ```python
211
+ client = AsyncChainClient(
212
+ chain_id="your-chain-id", # Your unique chain identifier
213
+ api_key="your-api-key", # Your API key for authentication
214
+ base_url="https://chainix.ai", # Base URL (optional, defaults to chainix.ai)
215
+ max_wait_time=300, # Max wait time in seconds (optional, default 300)
216
+ poll_interval=5, # How often to check status in seconds (optional, default 5, minimum 3)
217
+ verbose=True # Whether to print status messages (optional, default True)
218
+ )
219
+ ```
220
+
221
+ ### Silent Mode
222
+
223
+ For production environments or when you don't want status messages, you can disable verbose output:
224
+
225
+ ```python
226
+ client = AsyncChainClient(
227
+ chain_id="your-chain-id",
228
+ api_key="your-api-key",
229
+ verbose=False # Runs silently
230
+ )
231
+ ```
232
+
233
+ ## Running Chains
234
+
235
+ ### Basic Usage
236
+
237
+ ```python
238
+ result = client.run_chain(
239
+ initial_variables={
240
+ 'message body': 'Hi, please cancel my order',
241
+ 'user email': 'user@example.com',
242
+ 'order id': '12345',
243
+ }
244
+ )
245
+ ```
246
+
247
+ ### Test Mode
248
+
249
+ You can run chains in test mode for development and debugging:
250
+
251
+ ```python
252
+ result = client.run_chain(
253
+ initial_variables={
254
+ 'message body': 'Hi, please cancel my order',
255
+ 'user email': 'user@example.com',
256
+ 'order id': '12345'
257
+ },
258
+ test=True # Runs in test mode
259
+ )
260
+ ```
261
+
262
+ ## Error Handling
263
+
264
+ The client automatically handles several types of errors:
265
+
266
+ - **Network errors**: Automatically retries with backoff
267
+ - **Function execution errors**: Functions that throw exceptions are treated as failed (`success: False`)
268
+ - **Invalid function returns**: If functions don't return the required structure, the chain will stop with a clear error message
269
+
270
+ ### Best Practices
271
+
272
+ 1. **Always wrap the body of your custom function in try-catch blocks**, catch any errors and set success to false in the return dictionary
273
+ 2. **Return meaningful error information** when functions fail
274
+ 3. **Validate input arguments** at the start of your functions
275
+ 4. **Use exact variable names** in `vars_to_update` that match your variables names on the chain configuration on chainix.ai
276
+
277
+ ```python
278
+ def robust_function(args):
279
+ try:
280
+ # Validate inputs
281
+ if 'required_field' not in args:
282
+ raise ValueError("Missing required_field")
283
+
284
+ # Your business logic
285
+ result = perform_business_logic(args)
286
+
287
+ return {
288
+ 'success': True,
289
+ 'vars_to_update': {
290
+ 'operation result': result,
291
+ 'timestamp': datetime.now().isoformat()
292
+ }
293
+ }
294
+
295
+ except ValueError as e:
296
+ print(f"Validation error: {e}")
297
+ return {
298
+ 'success': False,
299
+ 'vars_to_update': {'error type': 'validation_error'}
300
+ }
301
+ except Exception as e:
302
+ print(f"Unexpected error: {e}")
303
+ return {
304
+ 'success': False,
305
+ 'vars_to_update': {'error type': 'unexpected_error'}
306
+ }
307
+ ```
308
+
309
+ ## License
310
+
311
+ MIT License
@@ -0,0 +1,6 @@
1
+ chainix/__init__.py,sha256=TbC8Nvsgo3n8s8IYr10U5qPc086Bn5ZxCl-lFfmhPe0,88
2
+ chainix/main.py,sha256=ACrRB8RXU2qOViH69oCyKLjZ2FIU8FiDPU8_HWLfPqw,8656
3
+ chainix-0.1.0.dist-info/METADATA,sha256=DcxsJbzx6k354W9ihrgM2E0PU1DzEUXp8gOBZJm7VDY,8915
4
+ chainix-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
5
+ chainix-0.1.0.dist-info/top_level.txt,sha256=_urHgybqjX2rAy9bI04VrZGYvBXaZq6cjuUic5gkuLY,8
6
+ chainix-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ chainix