airbyte-agent-klaviyo 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.
- airbyte_agent_klaviyo/__init__.py +225 -0
- airbyte_agent_klaviyo/_vendored/__init__.py +1 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/__init__.py +82 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/auth_strategies.py +1171 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/auth_template.py +135 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/cloud_utils/__init__.py +5 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/cloud_utils/client.py +213 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/connector_model_loader.py +1120 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/constants.py +78 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/exceptions.py +23 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/executor/__init__.py +31 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/executor/hosted_executor.py +201 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/executor/local_executor.py +1854 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/executor/models.py +202 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/extensions.py +693 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/http/__init__.py +37 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/http/adapters/__init__.py +9 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/http/adapters/httpx_adapter.py +251 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/http/config.py +98 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/http/exceptions.py +119 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/http/protocols.py +114 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/http/response.py +104 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/http_client.py +693 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/introspection.py +481 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/logging/__init__.py +11 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/logging/logger.py +273 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/logging/types.py +93 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/observability/__init__.py +11 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/observability/config.py +179 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/observability/models.py +19 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/observability/redactor.py +81 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/observability/session.py +103 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/performance/__init__.py +6 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/performance/instrumentation.py +57 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/performance/metrics.py +93 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/schema/__init__.py +75 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/schema/base.py +201 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/schema/components.py +244 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/schema/connector.py +120 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/schema/extensions.py +301 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/schema/operations.py +156 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/schema/security.py +236 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/secrets.py +182 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/telemetry/__init__.py +10 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/telemetry/config.py +32 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/telemetry/events.py +59 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/telemetry/tracker.py +155 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/types.py +270 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/utils.py +60 -0
- airbyte_agent_klaviyo/_vendored/connector_sdk/validation.py +848 -0
- airbyte_agent_klaviyo/connector.py +1431 -0
- airbyte_agent_klaviyo/connector_model.py +2230 -0
- airbyte_agent_klaviyo/models.py +676 -0
- airbyte_agent_klaviyo/types.py +1319 -0
- airbyte_agent_klaviyo-0.1.0.dist-info/METADATA +151 -0
- airbyte_agent_klaviyo-0.1.0.dist-info/RECORD +57 -0
- airbyte_agent_klaviyo-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared introspection utilities for connector metadata.
|
|
3
|
+
|
|
4
|
+
This module provides utilities for introspecting connector metadata,
|
|
5
|
+
generating descriptions, and formatting parameter signatures. These
|
|
6
|
+
functions are used by both the runtime decorators and the generated
|
|
7
|
+
connector code.
|
|
8
|
+
|
|
9
|
+
The module is designed to work with any object conforming to the
|
|
10
|
+
ConnectorModel and EndpointDefinition interfaces from connector_sdk.types.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Any, Protocol
|
|
16
|
+
|
|
17
|
+
# Constants
|
|
18
|
+
MAX_EXAMPLE_QUESTIONS = 5 # Maximum number of example questions to include in description
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _type_includes(type_value: Any, target: str) -> bool:
|
|
22
|
+
if isinstance(type_value, list):
|
|
23
|
+
return target in type_value
|
|
24
|
+
return type_value == target
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _is_object_schema(schema: dict[str, Any]) -> bool:
|
|
28
|
+
if "properties" in schema:
|
|
29
|
+
return True
|
|
30
|
+
return _type_includes(schema.get("type"), "object")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _is_array_schema(schema: dict[str, Any]) -> bool:
|
|
34
|
+
if "items" in schema:
|
|
35
|
+
return True
|
|
36
|
+
return _type_includes(schema.get("type"), "array")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _dedupe_param_entries(entries: list[tuple[str, bool]]) -> list[tuple[str, bool]]:
|
|
40
|
+
seen: dict[str, bool] = {}
|
|
41
|
+
ordered: list[str] = []
|
|
42
|
+
for name, required in entries:
|
|
43
|
+
if name not in seen:
|
|
44
|
+
seen[name] = required
|
|
45
|
+
ordered.append(name)
|
|
46
|
+
else:
|
|
47
|
+
seen[name] = seen[name] or required
|
|
48
|
+
return [(name, seen[name]) for name in ordered]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _flatten_schema_params(
|
|
52
|
+
schema: dict[str, Any],
|
|
53
|
+
prefix: str = "",
|
|
54
|
+
parent_required: bool = True,
|
|
55
|
+
seen_stack: set[int] | None = None,
|
|
56
|
+
) -> list[tuple[str, bool]]:
|
|
57
|
+
if not isinstance(schema, dict):
|
|
58
|
+
return []
|
|
59
|
+
|
|
60
|
+
if seen_stack is None:
|
|
61
|
+
seen_stack = set()
|
|
62
|
+
|
|
63
|
+
schema_id = id(schema)
|
|
64
|
+
if schema_id in seen_stack:
|
|
65
|
+
return []
|
|
66
|
+
|
|
67
|
+
seen_stack.add(schema_id)
|
|
68
|
+
try:
|
|
69
|
+
entries: list[tuple[str, bool]] = []
|
|
70
|
+
|
|
71
|
+
for subschema in schema.get("allOf", []) or []:
|
|
72
|
+
if isinstance(subschema, dict):
|
|
73
|
+
entries.extend(_flatten_schema_params(subschema, prefix, parent_required, seen_stack))
|
|
74
|
+
|
|
75
|
+
for keyword in ("anyOf", "oneOf"):
|
|
76
|
+
for subschema in schema.get(keyword, []) or []:
|
|
77
|
+
if isinstance(subschema, dict):
|
|
78
|
+
entries.extend(_flatten_schema_params(subschema, prefix, False, seen_stack))
|
|
79
|
+
|
|
80
|
+
properties = schema.get("properties")
|
|
81
|
+
if isinstance(properties, dict):
|
|
82
|
+
required_fields = set(schema.get("required", [])) if isinstance(schema.get("required"), list) else set()
|
|
83
|
+
for prop_name, prop_schema in properties.items():
|
|
84
|
+
path = f"{prefix}{prop_name}" if prefix else prop_name
|
|
85
|
+
is_required = parent_required and prop_name in required_fields
|
|
86
|
+
entries.append((path, is_required))
|
|
87
|
+
|
|
88
|
+
if isinstance(prop_schema, dict):
|
|
89
|
+
if _is_array_schema(prop_schema):
|
|
90
|
+
array_path = f"{path}[]"
|
|
91
|
+
entries.append((array_path, is_required))
|
|
92
|
+
items = prop_schema.get("items")
|
|
93
|
+
if isinstance(items, dict):
|
|
94
|
+
entries.extend(_flatten_schema_params(items, prefix=f"{array_path}.", parent_required=is_required, seen_stack=seen_stack))
|
|
95
|
+
if _is_object_schema(prop_schema):
|
|
96
|
+
entries.extend(_flatten_schema_params(prop_schema, prefix=f"{path}.", parent_required=is_required, seen_stack=seen_stack))
|
|
97
|
+
|
|
98
|
+
return _dedupe_param_entries(entries)
|
|
99
|
+
finally:
|
|
100
|
+
seen_stack.remove(schema_id)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _cache_field_value(field: Any, key: str) -> Any:
|
|
104
|
+
if isinstance(field, dict):
|
|
105
|
+
return field.get(key)
|
|
106
|
+
return getattr(field, key, None)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _flatten_cache_properties(properties: dict[str, Any], prefix: str) -> list[str]:
|
|
110
|
+
entries: list[str] = []
|
|
111
|
+
for prop_name, prop in properties.items():
|
|
112
|
+
path = f"{prefix}{prop_name}" if prefix else prop_name
|
|
113
|
+
entries.append(path)
|
|
114
|
+
|
|
115
|
+
prop_type = _cache_field_value(prop, "type")
|
|
116
|
+
prop_properties = _cache_field_value(prop, "properties")
|
|
117
|
+
|
|
118
|
+
if _type_includes(prop_type, "array"):
|
|
119
|
+
array_path = f"{path}[]"
|
|
120
|
+
entries.append(array_path)
|
|
121
|
+
if isinstance(prop_properties, dict):
|
|
122
|
+
entries.extend(_flatten_cache_properties(prop_properties, prefix=f"{array_path}."))
|
|
123
|
+
elif isinstance(prop_properties, dict):
|
|
124
|
+
entries.extend(_flatten_cache_properties(prop_properties, prefix=f"{path}."))
|
|
125
|
+
|
|
126
|
+
return entries
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _flatten_cache_field_paths(field: Any) -> list[str]:
|
|
130
|
+
field_name = _cache_field_value(field, "name")
|
|
131
|
+
if not isinstance(field_name, str) or not field_name:
|
|
132
|
+
return []
|
|
133
|
+
|
|
134
|
+
field_type = _cache_field_value(field, "type")
|
|
135
|
+
field_properties = _cache_field_value(field, "properties")
|
|
136
|
+
|
|
137
|
+
entries = [field_name]
|
|
138
|
+
if _type_includes(field_type, "array"):
|
|
139
|
+
array_path = f"{field_name}[]"
|
|
140
|
+
entries.append(array_path)
|
|
141
|
+
if isinstance(field_properties, dict):
|
|
142
|
+
entries.extend(_flatten_cache_properties(field_properties, prefix=f"{array_path}."))
|
|
143
|
+
elif isinstance(field_properties, dict):
|
|
144
|
+
entries.extend(_flatten_cache_properties(field_properties, prefix=f"{field_name}."))
|
|
145
|
+
|
|
146
|
+
return entries
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _dedupe_strings(values: list[str]) -> list[str]:
|
|
150
|
+
seen: set[str] = set()
|
|
151
|
+
ordered: list[str] = []
|
|
152
|
+
for value in values:
|
|
153
|
+
if value not in seen:
|
|
154
|
+
seen.add(value)
|
|
155
|
+
ordered.append(value)
|
|
156
|
+
return ordered
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _collect_search_field_paths(model: ConnectorModelProtocol) -> dict[str, list[str]]:
|
|
160
|
+
search_field_paths = getattr(model, "search_field_paths", None)
|
|
161
|
+
if isinstance(search_field_paths, dict) and search_field_paths:
|
|
162
|
+
normalized: dict[str, list[str]] = {}
|
|
163
|
+
for entity, fields in search_field_paths.items():
|
|
164
|
+
if not isinstance(entity, str) or not entity:
|
|
165
|
+
continue
|
|
166
|
+
if isinstance(fields, list):
|
|
167
|
+
normalized[entity] = _dedupe_strings([field for field in fields if isinstance(field, str) and field])
|
|
168
|
+
return normalized
|
|
169
|
+
|
|
170
|
+
openapi_spec = getattr(model, "openapi_spec", None)
|
|
171
|
+
info = getattr(openapi_spec, "info", None)
|
|
172
|
+
cache_config = getattr(info, "x_airbyte_cache", None)
|
|
173
|
+
entities = getattr(cache_config, "entities", None)
|
|
174
|
+
if not isinstance(entities, list):
|
|
175
|
+
return {}
|
|
176
|
+
|
|
177
|
+
search_fields: dict[str, list[str]] = {}
|
|
178
|
+
for entity in entities:
|
|
179
|
+
entity_name = _cache_field_value(entity, "entity")
|
|
180
|
+
if not isinstance(entity_name, str) or not entity_name:
|
|
181
|
+
continue
|
|
182
|
+
|
|
183
|
+
fields = _cache_field_value(entity, "fields") or []
|
|
184
|
+
if not isinstance(fields, list):
|
|
185
|
+
continue
|
|
186
|
+
field_paths: list[str] = []
|
|
187
|
+
for field in fields:
|
|
188
|
+
field_paths.extend(_flatten_cache_field_paths(field))
|
|
189
|
+
|
|
190
|
+
search_fields[entity_name] = _dedupe_strings(field_paths)
|
|
191
|
+
|
|
192
|
+
return search_fields
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _format_search_param_signature() -> str:
|
|
196
|
+
params = ["query*", "limit?", "cursor?", "fields?"]
|
|
197
|
+
return f"({', '.join(params)})"
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class EndpointProtocol(Protocol):
|
|
201
|
+
"""Protocol defining the expected interface for endpoint parameters.
|
|
202
|
+
|
|
203
|
+
This allows functions to work with any endpoint-like object
|
|
204
|
+
that has these attributes, including EndpointDefinition and mock objects.
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
path_params: list[str]
|
|
208
|
+
path_params_schema: dict[str, dict[str, Any]]
|
|
209
|
+
query_params: list[str]
|
|
210
|
+
query_params_schema: dict[str, dict[str, Any]]
|
|
211
|
+
body_fields: list[str]
|
|
212
|
+
request_schema: dict[str, Any] | None
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class EntityProtocol(Protocol):
|
|
216
|
+
"""Protocol defining the expected interface for entity definitions."""
|
|
217
|
+
|
|
218
|
+
name: str
|
|
219
|
+
actions: list[Any]
|
|
220
|
+
endpoints: dict[Any, EndpointProtocol]
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class ConnectorModelProtocol(Protocol):
|
|
224
|
+
"""Protocol defining the expected interface for connector model parameters.
|
|
225
|
+
|
|
226
|
+
This allows functions to work with any connector-like object
|
|
227
|
+
that has these attributes, including ConnectorModel and mock objects.
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
@property
|
|
231
|
+
def entities(self) -> list[EntityProtocol]: ...
|
|
232
|
+
|
|
233
|
+
@property
|
|
234
|
+
def openapi_spec(self) -> Any: ...
|
|
235
|
+
|
|
236
|
+
@property
|
|
237
|
+
def search_field_paths(self) -> dict[str, list[str]] | None: ...
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def format_param_signature(endpoint: EndpointProtocol) -> str:
|
|
241
|
+
"""Format parameter signature for an endpoint action.
|
|
242
|
+
|
|
243
|
+
Returns a string like: (id*) or (limit?, starting_after?, email?)
|
|
244
|
+
where * = required, ? = optional
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
endpoint: Object conforming to EndpointProtocol (e.g., EndpointDefinition)
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
Formatted parameter signature string
|
|
251
|
+
"""
|
|
252
|
+
params = []
|
|
253
|
+
|
|
254
|
+
# Defensive: safely access attributes with defaults for malformed endpoints
|
|
255
|
+
path_params = getattr(endpoint, "path_params", []) or []
|
|
256
|
+
query_params = getattr(endpoint, "query_params", []) or []
|
|
257
|
+
query_params_schema = getattr(endpoint, "query_params_schema", {}) or {}
|
|
258
|
+
body_fields = getattr(endpoint, "body_fields", []) or []
|
|
259
|
+
request_schema = getattr(endpoint, "request_schema", None)
|
|
260
|
+
|
|
261
|
+
# Path params (always required)
|
|
262
|
+
for name in path_params:
|
|
263
|
+
params.append(f"{name}*")
|
|
264
|
+
|
|
265
|
+
# Query params
|
|
266
|
+
for name in query_params:
|
|
267
|
+
schema = query_params_schema.get(name, {})
|
|
268
|
+
required = schema.get("required", False)
|
|
269
|
+
params.append(f"{name}{'*' if required else '?'}")
|
|
270
|
+
|
|
271
|
+
# Body fields (include nested params from schema when available)
|
|
272
|
+
if isinstance(request_schema, dict):
|
|
273
|
+
for name, required in _flatten_schema_params(request_schema):
|
|
274
|
+
params.append(f"{name}{'*' if required else '?'}")
|
|
275
|
+
elif request_schema:
|
|
276
|
+
required_fields = set(request_schema.get("required", [])) if isinstance(request_schema, dict) else set()
|
|
277
|
+
for name in body_fields:
|
|
278
|
+
params.append(f"{name}{'*' if name in required_fields else '?'}")
|
|
279
|
+
|
|
280
|
+
return f"({', '.join(params)})" if params else "()"
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def describe_entities(model: ConnectorModelProtocol) -> list[dict[str, Any]]:
|
|
284
|
+
"""Generate entity descriptions from ConnectorModel.
|
|
285
|
+
|
|
286
|
+
Returns a list of entity descriptions with detailed parameter information
|
|
287
|
+
for each action. This is used by generated connectors' list_entities() method.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
model: Object conforming to ConnectorModelProtocol (e.g., ConnectorModel)
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
List of entity description dicts with keys:
|
|
294
|
+
- entity_name: Name of the entity (e.g., "contacts", "deals")
|
|
295
|
+
- description: Entity description from the first endpoint
|
|
296
|
+
- available_actions: List of actions (e.g., ["list", "get", "create"])
|
|
297
|
+
- parameters: Dict mapping action -> list of parameter dicts
|
|
298
|
+
"""
|
|
299
|
+
entities = []
|
|
300
|
+
for entity_def in model.entities:
|
|
301
|
+
description = ""
|
|
302
|
+
parameters: dict[str, list[dict[str, Any]]] = {}
|
|
303
|
+
|
|
304
|
+
endpoints = getattr(entity_def, "endpoints", {}) or {}
|
|
305
|
+
if endpoints:
|
|
306
|
+
for action, endpoint in endpoints.items():
|
|
307
|
+
# Get description from first endpoint that has one
|
|
308
|
+
if not description:
|
|
309
|
+
endpoint_desc = getattr(endpoint, "description", None)
|
|
310
|
+
if endpoint_desc:
|
|
311
|
+
description = endpoint_desc
|
|
312
|
+
|
|
313
|
+
action_params: list[dict[str, Any]] = []
|
|
314
|
+
|
|
315
|
+
# Defensive: safely access endpoint attributes
|
|
316
|
+
path_params = getattr(endpoint, "path_params", []) or []
|
|
317
|
+
path_params_schema = getattr(endpoint, "path_params_schema", {}) or {}
|
|
318
|
+
query_params = getattr(endpoint, "query_params", []) or []
|
|
319
|
+
query_params_schema = getattr(endpoint, "query_params_schema", {}) or {}
|
|
320
|
+
body_fields = getattr(endpoint, "body_fields", []) or []
|
|
321
|
+
request_schema = getattr(endpoint, "request_schema", None)
|
|
322
|
+
|
|
323
|
+
# Path params (always required)
|
|
324
|
+
for param_name in path_params:
|
|
325
|
+
schema = path_params_schema.get(param_name, {})
|
|
326
|
+
action_params.append(
|
|
327
|
+
{
|
|
328
|
+
"name": param_name,
|
|
329
|
+
"in": "path",
|
|
330
|
+
"required": True,
|
|
331
|
+
"type": schema.get("type", "string"),
|
|
332
|
+
"description": schema.get("description", ""),
|
|
333
|
+
}
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# Query params
|
|
337
|
+
for param_name in query_params:
|
|
338
|
+
schema = query_params_schema.get(param_name, {})
|
|
339
|
+
action_params.append(
|
|
340
|
+
{
|
|
341
|
+
"name": param_name,
|
|
342
|
+
"in": "query",
|
|
343
|
+
"required": schema.get("required", False),
|
|
344
|
+
"type": schema.get("type", "string"),
|
|
345
|
+
"description": schema.get("description", ""),
|
|
346
|
+
}
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
# Body fields
|
|
350
|
+
if request_schema:
|
|
351
|
+
required_fields = request_schema.get("required", [])
|
|
352
|
+
properties = request_schema.get("properties", {})
|
|
353
|
+
for param_name in body_fields:
|
|
354
|
+
prop = properties.get(param_name, {})
|
|
355
|
+
action_params.append(
|
|
356
|
+
{
|
|
357
|
+
"name": param_name,
|
|
358
|
+
"in": "body",
|
|
359
|
+
"required": param_name in required_fields,
|
|
360
|
+
"type": prop.get("type", "string"),
|
|
361
|
+
"description": prop.get("description", ""),
|
|
362
|
+
}
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
if action_params:
|
|
366
|
+
# Action is an enum, use .value to get string
|
|
367
|
+
action_key = action.value if hasattr(action, "value") else str(action)
|
|
368
|
+
parameters[action_key] = action_params
|
|
369
|
+
|
|
370
|
+
actions = getattr(entity_def, "actions", []) or []
|
|
371
|
+
entities.append(
|
|
372
|
+
{
|
|
373
|
+
"entity_name": entity_def.name,
|
|
374
|
+
"description": description,
|
|
375
|
+
"available_actions": [a.value if hasattr(a, "value") else str(a) for a in actions],
|
|
376
|
+
"parameters": parameters,
|
|
377
|
+
}
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
return entities
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def generate_tool_description(
|
|
384
|
+
model: ConnectorModelProtocol,
|
|
385
|
+
*,
|
|
386
|
+
enable_hosted_mode_features: bool = True,
|
|
387
|
+
) -> str:
|
|
388
|
+
"""Generate AI tool description from connector metadata.
|
|
389
|
+
|
|
390
|
+
Produces a detailed description that includes:
|
|
391
|
+
- Per-entity/action parameter signatures with required (*) and optional (?) markers
|
|
392
|
+
- Response structure documentation with pagination hints
|
|
393
|
+
- Example questions if available in the OpenAPI spec
|
|
394
|
+
|
|
395
|
+
This is used by the Connector.tool_utils decorator to populate function
|
|
396
|
+
docstrings for AI framework integration.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
model: Object conforming to ConnectorModelProtocol (e.g., ConnectorModel)
|
|
400
|
+
enable_hosted_mode_features: When False, omit hosted-mode search guidance from the docstring.
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
Formatted description string suitable for AI tool documentation
|
|
404
|
+
"""
|
|
405
|
+
lines = []
|
|
406
|
+
# NOTE: Do not insert blank lines in the docstring; pydantic-ai parsing truncates
|
|
407
|
+
# at the first empty line and only keeps the initial section.
|
|
408
|
+
|
|
409
|
+
# Entity/action parameter details (including pagination params like limit, starting_after)
|
|
410
|
+
search_field_paths = _collect_search_field_paths(model) if enable_hosted_mode_features else {}
|
|
411
|
+
# Avoid a "PARAMETERS:" header because some docstring parsers treat it as a params section marker.
|
|
412
|
+
lines.append("ENTITIES (ACTIONS + PARAMS):")
|
|
413
|
+
for entity in model.entities:
|
|
414
|
+
lines.append(f" {entity.name}:")
|
|
415
|
+
actions = getattr(entity, "actions", []) or []
|
|
416
|
+
endpoints = getattr(entity, "endpoints", {}) or {}
|
|
417
|
+
for action in actions:
|
|
418
|
+
action_str = action.value if hasattr(action, "value") else str(action)
|
|
419
|
+
endpoint = endpoints.get(action)
|
|
420
|
+
if endpoint:
|
|
421
|
+
param_sig = format_param_signature(endpoint)
|
|
422
|
+
lines.append(f" - {action_str}{param_sig}")
|
|
423
|
+
else:
|
|
424
|
+
lines.append(f" - {action_str}()")
|
|
425
|
+
if entity.name in search_field_paths:
|
|
426
|
+
search_sig = _format_search_param_signature()
|
|
427
|
+
lines.append(f" - search{search_sig}")
|
|
428
|
+
|
|
429
|
+
# Response structure (brief, includes pagination hint)
|
|
430
|
+
lines.append("RESPONSE STRUCTURE:")
|
|
431
|
+
lines.append(" - list/api_search: {data: [...], meta: {has_more: bool}}")
|
|
432
|
+
lines.append(" - get: Returns entity directly (no envelope)")
|
|
433
|
+
lines.append(" To paginate: pass starting_after=<last_id> while has_more is true")
|
|
434
|
+
|
|
435
|
+
lines.append("GUIDELINES:")
|
|
436
|
+
if enable_hosted_mode_features:
|
|
437
|
+
lines.append(' - Prefer cached search over direct API calls when using execute(): action="search" whenever possible.')
|
|
438
|
+
lines.append(" - Direct API actions (list/get/download) are slower and should be used only if search cannot answer the query.")
|
|
439
|
+
lines.append(" - Keep results small: use params.fields, params.query.filter, small params.limit, and cursor pagination.")
|
|
440
|
+
lines.append(" - If output is too large, refine the query with tighter filters/fields/limit.")
|
|
441
|
+
|
|
442
|
+
if search_field_paths:
|
|
443
|
+
lines.append("SEARCH (PREFERRED):")
|
|
444
|
+
lines.append(' execute(entity, action="search", params={')
|
|
445
|
+
lines.append(' "query": {"filter": <condition>, "sort": [{"field": "asc|desc"}, ...]},')
|
|
446
|
+
lines.append(' "limit": <int>, "cursor": <str>, "fields": ["field", "nested.field", ...]')
|
|
447
|
+
lines.append(" })")
|
|
448
|
+
lines.append(' Example: {"query": {"filter": {"eq": {"title": "Intro to Airbyte | Miinto"}}}, "limit": 1,')
|
|
449
|
+
lines.append(' "fields": ["id", "title", "started", "primaryUserId"]}')
|
|
450
|
+
lines.append(" Conditions are composable:")
|
|
451
|
+
lines.append(" - eq, neq, gt, gte, lt, lte, in, like, fuzzy, keyword, contains, any")
|
|
452
|
+
lines.append(' - and/or/not to combine conditions (e.g., {"and": [cond1, cond2]})')
|
|
453
|
+
|
|
454
|
+
lines.append("SEARCHABLE FIELDS:")
|
|
455
|
+
for entity_name, field_paths in search_field_paths.items():
|
|
456
|
+
if field_paths:
|
|
457
|
+
lines.append(f" {entity_name}: {', '.join(field_paths)}")
|
|
458
|
+
else:
|
|
459
|
+
lines.append(f" {entity_name}: (no fields listed)")
|
|
460
|
+
|
|
461
|
+
# Add example questions if available in openapi_spec
|
|
462
|
+
openapi_spec = getattr(model, "openapi_spec", None)
|
|
463
|
+
if openapi_spec:
|
|
464
|
+
info = getattr(openapi_spec, "info", None)
|
|
465
|
+
if info:
|
|
466
|
+
example_questions = getattr(info, "x_airbyte_example_questions", None)
|
|
467
|
+
if example_questions:
|
|
468
|
+
supported = getattr(example_questions, "supported", None)
|
|
469
|
+
if supported:
|
|
470
|
+
lines.append("EXAMPLE QUESTIONS:")
|
|
471
|
+
for q in supported[:MAX_EXAMPLE_QUESTIONS]:
|
|
472
|
+
lines.append(f" - {q}")
|
|
473
|
+
|
|
474
|
+
# Generic parameter description for function signature
|
|
475
|
+
lines.append("FUNCTION PARAMETERS:")
|
|
476
|
+
lines.append(" - entity: Entity name (string)")
|
|
477
|
+
lines.append(" - action: Operation to perform (string)")
|
|
478
|
+
lines.append(" - params: Operation parameters (dict) - see entity details above")
|
|
479
|
+
lines.append("Parameter markers: * = required, ? = optional")
|
|
480
|
+
|
|
481
|
+
return "\n".join(lines)
|