Coverage for open_webui/utils/actions.py: 24%

73 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-07 05:07 +0000

1import inspect 

2import logging 

3import sys 

4from typing import Any 

5 

6from fastapi import Request 

7from open_webui.env import ENABLE_PLUGINS, GLOBAL_LOG_LEVEL 

8from open_webui.models.functions import Functions 

9from open_webui.models.users import UserModel 

10from open_webui.socket.main import get_event_call, get_event_emitter 

11from open_webui.utils.middleware import process_tool_result 

12from open_webui.utils.models import check_model_access, get_all_models 

13from open_webui.utils.plugin import get_function_module_from_cache 

14 

15logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL) 

16log = logging.getLogger(__name__) 

17 

18 

19async def chat_action(request: Request, action_id: str, form_data: dict, user: Any): 

20 if not ENABLE_PLUGINS: 20 ↛ 21line 20 didn't jump to line 21 because the condition on line 20 was never true

21 raise Exception('Plugins are disabled by ENABLE_PLUGINS=false') 

22 

23 if '.' in action_id: 

24 action_id, sub_action_id = action_id.split('.') 

25 else: 

26 sub_action_id = None 

27 

28 action = await Functions.get_function_by_id(action_id) 

29 if not action: 29 ↛ 32line 29 didn't jump to line 32 because the condition on line 29 was always true

30 raise Exception(f'Action not found: {action_id}') 

31 

32 if not request.app.state.MODELS: 

33 await get_all_models(request, user=user) 

34 

35 if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'): 

36 models = { 

37 request.state.model['id']: request.state.model, 

38 } 

39 else: 

40 models = request.app.state.MODELS 

41 

42 data = form_data 

43 model_id = data['model'] 

44 

45 if model_id not in models: 

46 raise Exception('Model not found') 

47 model = models[model_id] 

48 

49 # Availability gate — keep this route consistent with the actions a model 

50 # actually surfaces to the client. Executing admin-authored Function code is 

51 # intended; this only stops a disabled, unassigned, or access-restricted 

52 # action from being reached by calling the route with a raw action_id. 

53 if action.type != 'action' or not action.is_active: 

54 raise Exception(f'Action not available: {action_id}') 

55 

56 # Direct connections carry a client-supplied model the caller already owns, 

57 # so scope the model-bound checks to server-resolved models. 

58 if not getattr(request.state, 'direct', False) and user.role != 'admin': 

59 await check_model_access(user, model) 

60 # model['actions'] entries are '<function_id>' or '<function_id>.<sub_id>'; 

61 # the function id is always the prefix. 

62 surfaced_action_ids = {item.get('id', '').split('.', 1)[0] for item in model.get('actions', [])} 

63 if action_id not in surfaced_action_ids: 

64 raise Exception(f'Action not available: {action_id}') 

65 

66 __event_emitter__ = await get_event_emitter( 

67 { 

68 'chat_id': data['chat_id'], 

69 'message_id': data['id'], 

70 'session_id': data['session_id'], 

71 'user_id': user.id, 

72 } 

73 ) 

74 __event_call__ = await get_event_call( 

75 { 

76 'chat_id': data['chat_id'], 

77 'message_id': data['id'], 

78 'session_id': data['session_id'], 

79 'user_id': user.id, 

80 } 

81 ) 

82 

83 function_module, _, _ = await get_function_module_from_cache(request, action_id) 

84 

85 if hasattr(function_module, 'valves') and hasattr(function_module, 'Valves'): 

86 valves = await Functions.get_function_valves_by_id(action_id) 

87 function_module.valves = function_module.Valves(**(valves if valves else {})) 

88 

89 if hasattr(function_module, 'action'): 

90 try: 

91 action = function_module.action 

92 

93 # Get the signature of the function 

94 sig = inspect.signature(action) 

95 params = {'body': data} 

96 

97 # Extra parameters to be passed to the function 

98 extra_params = { 

99 '__model__': model, 

100 '__id__': sub_action_id if sub_action_id is not None else action_id, 

101 '__event_emitter__': __event_emitter__, 

102 '__event_call__': __event_call__, 

103 '__request__': request, 

104 } 

105 

106 # Add extra params in contained in function signature 

107 for key, value in extra_params.items(): 

108 if key in sig.parameters: 

109 params[key] = value 

110 

111 if '__user__' in sig.parameters: 

112 __user__ = user.model_dump() if isinstance(user, UserModel) else {} 

113 

114 try: 

115 if hasattr(function_module, 'UserValves'): 

116 __user__['valves'] = function_module.UserValves( 

117 **await Functions.get_user_valves_by_id_and_user_id(action_id, user.id) 

118 ) 

119 except Exception as e: 

120 log.exception(f'Failed to get user values: {e}') 

121 

122 params = {**params, '__user__': __user__} 

123 

124 if inspect.iscoroutinefunction(action): 

125 data = await action(**params) 

126 else: 

127 data = action(**params) 

128 

129 # Process action result for Rich UI embeds (HTMLResponse, tuple with headers) 

130 processed_result, _, action_embeds = await process_tool_result( 

131 request, 

132 action_id, 

133 data, 

134 'action', 

135 ) 

136 

137 if action_embeds: 

138 await __event_emitter__( 

139 { 

140 'type': 'embeds', 

141 'data': { 

142 'embeds': action_embeds, 

143 }, 

144 } 

145 ) 

146 # Replace data with the processed status dict so we don't 

147 # try to serialize the raw HTMLResponse / tuple back to the client 

148 data = processed_result 

149 

150 except Exception as e: 

151 raise Exception(f'Error: {e}') 

152 

153 return data