|
1 | 1 | import contextlib |
| 2 | +import inspect |
2 | 3 | import json |
3 | 4 | import logging |
4 | 5 | import os |
|
12 | 13 |
|
13 | 14 | from . import dao |
14 | 15 | from . import table_schema as ts |
| 16 | +from .exceptions import UserException |
15 | 17 | from .interface import CommonInterface |
16 | 18 | from .sync_actions import SyncActionResult, process_sync_action_result |
17 | 19 |
|
@@ -249,6 +251,103 @@ def execute_action(self): |
249 | 251 | raise AttributeError(f"The defined action {action} is not implemented!") from e |
250 | 252 | return action_method() |
251 | 253 |
|
| 254 | + def get_agent_schema(self) -> Optional[dict]: |
| 255 | + """Override in component to provide a custom agent schema. |
| 256 | +
|
| 257 | + If not overridden, the schema is auto-generated via introspection. |
| 258 | + Return ``None`` to explicitly disable agent mode for this component. |
| 259 | + """ |
| 260 | + return self._generate_agent_schema() |
| 261 | + |
| 262 | + def get_agent_context(self) -> Optional[dict]: |
| 263 | + """Override in component to provide execution context for agentCode. |
| 264 | +
|
| 265 | + Should return a dict of variable names to initialized objects that |
| 266 | + will be available in the agent code namespace. |
| 267 | + For example: ``{"sf": authenticated_salesforce_client}`` |
| 268 | +
|
| 269 | + Returns ``None`` by default (agent code runs with ``comp`` only). |
| 270 | + """ |
| 271 | + return None |
| 272 | + |
| 273 | + @sync_action('agentSchema') |
| 274 | + def agent_schema(self): |
| 275 | + schema = self.get_agent_schema() |
| 276 | + if schema is None: |
| 277 | + raise UserException("This component does not support agent mode.") |
| 278 | + return schema |
| 279 | + |
| 280 | + @sync_action('agentCode') |
| 281 | + def agent_code(self): |
| 282 | + code = self.configuration.parameters.get('agent_code') |
| 283 | + if not code: |
| 284 | + raise UserException("Parameter 'agent_code' is required.") |
| 285 | + context = self.get_agent_context() |
| 286 | + local_ns = {'comp': self} |
| 287 | + if context: |
| 288 | + local_ns.update(context) |
| 289 | + exec(code, {'__builtins__': __builtins__}, local_ns) # noqa: S102 |
| 290 | + raw_result = local_ns.get('result') |
| 291 | + if raw_result is None: |
| 292 | + return None |
| 293 | + return {'result': raw_result} |
| 294 | + |
| 295 | + def _generate_agent_schema(self) -> dict: |
| 296 | + comp_class = type(self) |
| 297 | + comp_module = inspect.getmodule(comp_class) |
| 298 | + schema = { |
| 299 | + 'component_id': self.environment_variables.component_id, |
| 300 | + 'modules': {}, |
| 301 | + 'sync_actions': [], |
| 302 | + 'context_variables': {}, |
| 303 | + 'installed_packages': self._get_installed_packages(), |
| 304 | + } |
| 305 | + try: |
| 306 | + schema['modules']['component'] = inspect.getsource(comp_class) |
| 307 | + except (TypeError, OSError): |
| 308 | + pass |
| 309 | + if comp_module: |
| 310 | + for name, obj in vars(comp_module).items(): |
| 311 | + if name.startswith('_'): |
| 312 | + continue |
| 313 | + if inspect.isclass(obj) and obj is not comp_class: |
| 314 | + try: |
| 315 | + schema['modules'][name] = inspect.getsource(obj) |
| 316 | + except (TypeError, OSError): |
| 317 | + pass |
| 318 | + elif inspect.ismodule(obj): |
| 319 | + try: |
| 320 | + schema['modules'][name] = inspect.getsource(obj) |
| 321 | + except (TypeError, OSError): |
| 322 | + pass |
| 323 | + for action_name, method_name in _SYNC_ACTION_MAPPING.items(): |
| 324 | + if action_name in ('run', 'agentSchema', 'agentCode'): |
| 325 | + continue |
| 326 | + method = getattr(self, method_name, None) |
| 327 | + if method: |
| 328 | + schema['sync_actions'].append({ |
| 329 | + 'action': action_name, |
| 330 | + 'method': method_name, |
| 331 | + 'doc': inspect.getdoc(method) or '', |
| 332 | + }) |
| 333 | + context = self.get_agent_context() |
| 334 | + if context: |
| 335 | + for var_name, obj in context.items(): |
| 336 | + schema['context_variables'][var_name] = { |
| 337 | + 'type': type(obj).__qualname__, |
| 338 | + 'module': type(obj).__module__, |
| 339 | + 'methods': [m for m in dir(obj) if not m.startswith('_')], |
| 340 | + } |
| 341 | + return schema |
| 342 | + |
| 343 | + @staticmethod |
| 344 | + def _get_installed_packages() -> List[str]: |
| 345 | + try: |
| 346 | + from importlib.metadata import distributions |
| 347 | + return sorted([f"{d.metadata['Name']}=={d.metadata['Version']}" for d in distributions()]) |
| 348 | + except Exception: |
| 349 | + return [] |
| 350 | + |
252 | 351 | def _generate_table_metadata_legacy(self, table_schema: ts.TableSchema) -> dao.TableMetadata: |
253 | 352 | """ |
254 | 353 | Generates a TableMetadata object for the table definition using a TableSchema object. |
|
0 commit comments