Skip to content

Commit a5c116f

Browse files
feat: add agentSchema and agentCode sync actions to ComponentBase
Co-Authored-By: zdenek.srotyr@keboola.com <zdenek.srotyr@keboola.com>
1 parent 9854034 commit a5c116f

File tree

4 files changed

+378
-0
lines changed

4 files changed

+378
-0
lines changed

src/keboola/component/base.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import contextlib
2+
import inspect
23
import json
34
import logging
45
import os
@@ -12,6 +13,7 @@
1213

1314
from . import dao
1415
from . import table_schema as ts
16+
from .exceptions import UserException
1517
from .interface import CommonInterface
1618
from .sync_actions import SyncActionResult, process_sync_action_result
1719

@@ -249,6 +251,103 @@ def execute_action(self):
249251
raise AttributeError(f"The defined action {action} is not implemented!") from e
250252
return action_method()
251253

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+
252351
def _generate_table_metadata_legacy(self, table_schema: ts.TableSchema) -> dao.TableMetadata:
253352
"""
254353
Generates a TableMetadata object for the table definition using a TableSchema object.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"storage": {"input": {"tables": []}, "output": {"tables": []}}, "parameters": {"agent_code": "result = 1 + 1"}, "action": "agentCode"}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"storage": {
3+
"input": {
4+
"tables": []
5+
},
6+
"output": {
7+
"tables": []
8+
}
9+
},
10+
"parameters": {},
11+
"action": "agentSchema"
12+
}

0 commit comments

Comments
 (0)