19
19
import inspect
20
20
import logging
21
21
import os
22
+ from importlib .machinery import ModuleSpec
22
23
from pathlib import Path
23
- from typing import Any , Dict , List , Optional , Tuple , Union
24
+ from typing import Any , Callable , Dict , List , Optional , Tuple , Type , Union , cast
24
25
25
26
from langchain .chains .base import Chain
26
27
from langchain_core .runnables import Runnable
@@ -51,7 +52,7 @@ def __init__(
51
52
"""
52
53
log .info ("Initializing action dispatcher" )
53
54
54
- self ._registered_actions = {}
55
+ self ._registered_actions : Dict [ str , Union [ Type , Callable [..., Any ]]] = {}
55
56
56
57
if load_all_actions :
57
58
# TODO: check for better way to find actions dir path or use constants.py
@@ -78,9 +79,12 @@ def __init__(
78
79
# Last, but not least, if there was a config path, we try to load actions
79
80
# from there as well.
80
81
if config_path :
81
- config_path = config_path .split ("," )
82
- for path in config_path :
83
- self .load_actions_from_path (Path (path .strip ()))
82
+ split_config_path : List [str ] = config_path .split ("," )
83
+
84
+ # Don't load actions if we have an empty list
85
+ if split_config_path :
86
+ for path in split_config_path :
87
+ self .load_actions_from_path (Path (path .strip ()))
84
88
85
89
# If there are any imported paths, we load the actions from there as well.
86
90
if import_paths :
@@ -120,26 +124,28 @@ def load_actions_from_path(self, path: Path):
120
124
)
121
125
122
126
def register_action (
123
- self , action : callable , name : Optional [str ] = None , override : bool = True
127
+ self , action : Callable , name : Optional [str ] = None , override : bool = True
124
128
):
125
129
"""Registers an action with the given name.
126
130
127
131
Args:
128
- action (callable ): The action function.
132
+ action (Callable ): The action function.
129
133
name (Optional[str]): The name of the action. Defaults to None.
130
134
override (bool): If an action already exists, whether it should be overridden or not.
131
135
"""
132
136
if name is None :
133
137
action_meta = getattr (action , "action_meta" , None )
134
- name = action_meta ["name" ] if action_meta else action .__name__
138
+ action_name = action_meta ["name" ] if action_meta else action .__name__
139
+ else :
140
+ action_name = name
135
141
136
142
# If we're not allowed to override, we stop.
137
- if name in self ._registered_actions and not override :
143
+ if action_name in self ._registered_actions and not override :
138
144
return
139
145
140
- self ._registered_actions [name ] = action
146
+ self ._registered_actions [action_name ] = action
141
147
142
- def register_actions (self , actions_obj : any , override : bool = True ):
148
+ def register_actions (self , actions_obj : Any , override : bool = True ):
143
149
"""Registers all the actions from the given object.
144
150
145
151
Args:
@@ -167,7 +173,7 @@ def has_registered(self, name: str) -> bool:
167
173
name = self ._normalize_action_name (name )
168
174
return name in self .registered_actions
169
175
170
- def get_action (self , name : str ) -> callable :
176
+ def get_action (self , name : str ) -> Optional [ Callable ] :
171
177
"""Get the registered action by name.
172
178
173
179
Args:
@@ -181,7 +187,7 @@ def get_action(self, name: str) -> callable:
181
187
182
188
async def execute_action (
183
189
self , action_name : str , params : Dict [str , Any ]
184
- ) -> Tuple [Union [str , Dict [str , Any ]], str ]:
190
+ ) -> Tuple [Union [Optional [ str ] , Dict [str , Any ]], str ]:
185
191
"""Execute a registered action.
186
192
187
193
Args:
@@ -195,16 +201,21 @@ async def execute_action(
195
201
action_name = self ._normalize_action_name (action_name )
196
202
197
203
if action_name in self ._registered_actions :
198
- log .info (f"Executing registered action: { action_name } " )
199
- fn = self ._registered_actions .get (action_name , None )
204
+ log .info ("Executing registered action: %s" , action_name )
205
+ maybe_fn : Optional [Callable ] = self ._registered_actions .get (
206
+ action_name , None
207
+ )
208
+ if not maybe_fn :
209
+ raise Exception (f"Action '{ action_name } ' is not registered." )
200
210
211
+ fn = cast (Callable , maybe_fn )
201
212
# Actions that are registered as classes are initialized lazy, when
202
213
# they are first used.
203
214
if inspect .isclass (fn ):
204
215
fn = fn ()
205
216
self ._registered_actions [action_name ] = fn
206
217
207
- if fn is not None :
218
+ if fn :
208
219
try :
209
220
# We support both functions and classes as actions
210
221
if inspect .isfunction (fn ) or inspect .ismethod (fn ):
@@ -245,7 +256,17 @@ async def execute_action(
245
256
result = await runnable .ainvoke (input = params )
246
257
else :
247
258
# TODO: there should be a common base class here
248
- result = fn .run (** params )
259
+ fn_run_func = getattr (fn , "run" , None )
260
+ if not callable (fn_run_func ):
261
+ raise Exception (
262
+ f"No 'run' method defined for action '{ action_name } '."
263
+ )
264
+
265
+ fn_run_func_with_signature = cast (
266
+ Callable [[], Union [Optional [str ], Dict [str , Any ]]],
267
+ fn_run_func ,
268
+ )
269
+ result = fn_run_func_with_signature (** params )
249
270
return result , "success"
250
271
251
272
# We forward LLM Call exceptions
@@ -288,6 +309,7 @@ def _load_actions_from_module(filepath: str):
288
309
"""
289
310
action_objects = {}
290
311
filename = os .path .basename (filepath )
312
+ module = None
291
313
292
314
if not os .path .isfile (filepath ):
293
315
log .error (f"{ filepath } does not exist or is not a file." )
@@ -298,13 +320,16 @@ def _load_actions_from_module(filepath: str):
298
320
log .debug (f"Analyzing file { filename } " )
299
321
# Import the module from the file
300
322
301
- spec = importlib .util .spec_from_file_location (filename , filepath )
302
- if spec is None :
323
+ spec : Optional [ModuleSpec ] = importlib .util .spec_from_file_location (
324
+ filename , filepath
325
+ )
326
+ if not spec :
303
327
log .error (f"Failed to create a module spec from { filepath } ." )
304
328
return action_objects
305
329
306
330
module = importlib .util .module_from_spec (spec )
307
- spec .loader .exec_module (module )
331
+ if spec .loader :
332
+ spec .loader .exec_module (module )
308
333
309
334
# Loop through all members in the module and check for the `@action` decorator
310
335
# If class has action decorator is_action class member is true
@@ -313,19 +338,25 @@ def _load_actions_from_module(filepath: str):
313
338
obj , "action_meta"
314
339
):
315
340
try :
316
- action_objects [obj .action_meta ["name" ]] = obj
317
- log .info (f"Added { obj .action_meta ['name' ]} to actions" )
341
+ actionable_name : str = getattr (obj , "action_meta" ).get ("name" )
342
+ action_objects [actionable_name ] = obj
343
+ log .info (f"Added { actionable_name } to actions" )
318
344
except Exception as e :
319
345
log .error (
320
- f"Failed to register { obj . action_meta [ ' name' ] } in action dispatcher due to exception { e } "
346
+ f"Failed to register { name } in action dispatcher due to exception { e } "
321
347
)
322
348
except Exception as e :
349
+ if module is None :
350
+ raise RuntimeError (f"Failed to load actions from module at { filepath } ." )
351
+ if not module .__file__ :
352
+ raise RuntimeError (f"No file found for module { module } at { filepath } ." )
353
+
323
354
try :
324
355
relative_filepath = Path (module .__file__ ).relative_to (Path .cwd ())
325
356
except ValueError :
326
357
relative_filepath = Path (module .__file__ ).resolve ()
327
358
log .error (
328
- f"Failed to register { filename } from { relative_filepath } in action dispatcher due to exception: { e } "
359
+ f"Failed to register { filename } in action dispatcher due to exception: { e } "
329
360
)
330
361
331
362
return action_objects
0 commit comments