airbyte-agent-slack 0.1.15__py3-none-any.whl → 0.1.25__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.
@@ -182,6 +182,77 @@ class CacheEntityConfig(BaseModel):
182
182
  return self.x_airbyte_name or self.entity
183
183
 
184
184
 
185
+ class ReplicationConfigProperty(BaseModel):
186
+ """
187
+ Property definition for replication configuration fields.
188
+
189
+ Defines a single field in the replication configuration with its type,
190
+ description, and optional default value.
191
+
192
+ Example YAML usage:
193
+ x-airbyte-replication-config:
194
+ properties:
195
+ start_date:
196
+ type: string
197
+ title: Start Date
198
+ description: UTC date and time from which to replicate data
199
+ format: date-time
200
+ """
201
+
202
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
203
+
204
+ type: str
205
+ title: str | None = None
206
+ description: str | None = None
207
+ format: str | None = None
208
+ default: str | int | float | bool | None = None
209
+ enum: list[str] | None = None
210
+
211
+
212
+ class ReplicationConfig(BaseModel):
213
+ """
214
+ Replication configuration extension (x-airbyte-replication-config).
215
+
216
+ Defines replication-specific settings for MULTI mode connectors that need
217
+ to configure the underlying replication connector. This allows users who
218
+ use the direct-style API (credentials + environment) to also specify
219
+ replication settings like start_date, lookback_window, etc.
220
+
221
+ This extension is added to the Info model and provides field definitions
222
+ for replication configuration that gets merged into the source config
223
+ when creating sources.
224
+
225
+ Example YAML usage:
226
+ info:
227
+ title: HubSpot API
228
+ x-airbyte-replication-config:
229
+ title: Replication Configuration
230
+ description: Settings for data replication
231
+ properties:
232
+ start_date:
233
+ type: string
234
+ title: Start Date
235
+ description: UTC date and time from which to replicate data
236
+ format: date-time
237
+ required:
238
+ - start_date
239
+ replication_config_key_mapping:
240
+ start_date: start_date
241
+ """
242
+
243
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
244
+
245
+ title: str | None = None
246
+ description: str | None = None
247
+ properties: dict[str, ReplicationConfigProperty] = Field(default_factory=dict)
248
+ required: list[str] = Field(default_factory=list)
249
+ replication_config_key_mapping: dict[str, str] = Field(
250
+ default_factory=dict,
251
+ alias="replication_config_key_mapping",
252
+ description="Mapping from replication_config field names to source_config field names",
253
+ )
254
+
255
+
185
256
  class CacheConfig(BaseModel):
186
257
  """
187
258
  Cache configuration extension (x-airbyte-cache).
@@ -252,3 +252,4 @@ class ConnectorModel(BaseModel):
252
252
  entities: list[EntityDefinition]
253
253
  openapi_spec: Any | None = None # Optional reference to OpenAPIConnector
254
254
  retry_config: RetryConfig | None = None # Optional retry configuration
255
+ search_field_paths: dict[str, list[str]] | None = None
@@ -4,8 +4,11 @@ Slack connector.
4
4
 
5
5
  from __future__ import annotations
6
6
 
7
+ import inspect
8
+ import json
7
9
  import logging
8
- from typing import TYPE_CHECKING, Any, Callable, TypeVar, overload
10
+ from functools import wraps
11
+ from typing import TYPE_CHECKING, Any, Callable, Mapping, TypeVar, overload
9
12
  try:
10
13
  from typing import Literal
11
14
  except ImportError:
@@ -27,6 +30,11 @@ from .types import (
27
30
  ThreadsListParams,
28
31
  UsersGetParams,
29
32
  UsersListParams,
33
+ AirbyteSearchParams,
34
+ ChannelsSearchFilter,
35
+ ChannelsSearchQuery,
36
+ UsersSearchFilter,
37
+ UsersSearchQuery,
30
38
  )
31
39
  if TYPE_CHECKING:
32
40
  from .models import SlackAuthConfig
@@ -46,11 +54,49 @@ from .models import (
46
54
  ReactionAddResponse,
47
55
  Thread,
48
56
  User,
57
+ AirbyteSearchHit,
58
+ AirbyteSearchResult,
59
+ ChannelsSearchData,
60
+ ChannelsSearchResult,
61
+ UsersSearchData,
62
+ UsersSearchResult,
49
63
  )
50
64
 
51
65
  # TypeVar for decorator type preservation
52
66
  _F = TypeVar("_F", bound=Callable[..., Any])
53
67
 
68
+ DEFAULT_MAX_OUTPUT_CHARS = 50_000 # ~50KB default, configurable per-tool
69
+
70
+
71
+ def _raise_output_too_large(message: str) -> None:
72
+ try:
73
+ from pydantic_ai import ModelRetry # type: ignore[import-not-found]
74
+ except Exception as exc:
75
+ raise RuntimeError(message) from exc
76
+ raise ModelRetry(message)
77
+
78
+
79
+ def _check_output_size(result: Any, max_chars: int | None, tool_name: str) -> Any:
80
+ if max_chars is None or max_chars <= 0:
81
+ return result
82
+
83
+ try:
84
+ serialized = json.dumps(result, default=str)
85
+ except (TypeError, ValueError):
86
+ return result
87
+
88
+ if len(serialized) > max_chars:
89
+ truncated_preview = serialized[:500] + "..." if len(serialized) > 500 else serialized
90
+ _raise_output_too_large(
91
+ f"Tool '{tool_name}' output too large ({len(serialized):,} chars, limit {max_chars:,}). "
92
+ "Please narrow your query by: using the 'fields' parameter to select only needed fields, "
93
+ "adding filters, or reducing the 'limit'. "
94
+ f"Preview: {truncated_preview}"
95
+ )
96
+
97
+ return result
98
+
99
+
54
100
 
55
101
 
56
102
  class SlackConnector:
@@ -61,7 +107,7 @@ class SlackConnector:
61
107
  """
62
108
 
63
109
  connector_name = "slack"
64
- connector_version = "0.1.5"
110
+ connector_version = "0.1.8"
65
111
  vendored_sdk_version = "0.1.0" # Version of vendored connector-sdk
66
112
 
67
113
  # Map of (entity, action) -> needs_envelope for envelope wrapping decision
@@ -123,7 +169,7 @@ class SlackConnector:
123
169
  Example: lambda tokens: save_to_database(tokens)
124
170
  Examples:
125
171
  # Local mode (direct API calls)
126
- connector = SlackConnector(auth_config=SlackAuthConfig(access_token="..."))
172
+ connector = SlackConnector(auth_config=SlackAuthConfig(api_token="..."))
127
173
  # Hosted mode (executed on Airbyte cloud)
128
174
  connector = SlackConnector(
129
175
  external_user_id="user-123",
@@ -303,15 +349,15 @@ class SlackConnector:
303
349
  async def execute(
304
350
  self,
305
351
  entity: str,
306
- action: str,
307
- params: dict[str, Any]
352
+ action: Literal["list", "get", "create", "update", "search"],
353
+ params: Mapping[str, Any]
308
354
  ) -> SlackExecuteResult[Any] | SlackExecuteResultWithMeta[Any, Any] | Any: ...
309
355
 
310
356
  async def execute(
311
357
  self,
312
358
  entity: str,
313
- action: str,
314
- params: dict[str, Any] | None = None
359
+ action: Literal["list", "get", "create", "update", "search"],
360
+ params: Mapping[str, Any] | None = None
315
361
  ) -> Any:
316
362
  """
317
363
  Execute an entity operation with full type safety.
@@ -339,16 +385,17 @@ class SlackConnector:
339
385
  from ._vendored.connector_sdk.executor import ExecutionConfig
340
386
 
341
387
  # Remap parameter names from snake_case (TypedDict keys) to API parameter names
342
- if params:
388
+ resolved_params = dict(params) if params is not None else None
389
+ if resolved_params:
343
390
  param_map = self._PARAM_MAP.get((entity, action), {})
344
391
  if param_map:
345
- params = {param_map.get(k, k): v for k, v in params.items()}
392
+ resolved_params = {param_map.get(k, k): v for k, v in resolved_params.items()}
346
393
 
347
394
  # Use ExecutionConfig for both local and hosted executors
348
395
  config = ExecutionConfig(
349
396
  entity=entity,
350
397
  action=action,
351
- params=params
398
+ params=resolved_params
352
399
  )
353
400
 
354
401
  result = await self._executor.execute(config)
@@ -375,41 +422,67 @@ class SlackConnector:
375
422
  # ===== INTROSPECTION METHODS =====
376
423
 
377
424
  @classmethod
378
- def describe(cls, func: _F) -> _F:
425
+ def tool_utils(
426
+ cls,
427
+ func: _F | None = None,
428
+ *,
429
+ update_docstring: bool = True,
430
+ max_output_chars: int | None = DEFAULT_MAX_OUTPUT_CHARS,
431
+ ) -> _F | Callable[[_F], _F]:
379
432
  """
380
- Decorator that populates a function's docstring with connector capabilities.
381
-
382
- This class method can be used as a decorator to automatically generate
383
- comprehensive documentation for AI tool functions.
433
+ Decorator that adds tool utilities like docstring augmentation and output limits.
384
434
 
385
435
  Usage:
386
436
  @mcp.tool()
387
- @SlackConnector.describe
437
+ @SlackConnector.tool_utils
388
438
  async def execute(entity: str, action: str, params: dict):
389
- '''Execute operations.'''
390
439
  ...
391
440
 
392
- The decorated function's __doc__ will be updated with:
393
- - Available entities and their actions
394
- - Parameter signatures with required (*) and optional (?) markers
395
- - Response structure documentation
396
- - Example questions (if available in OpenAPI spec)
441
+ @mcp.tool()
442
+ @SlackConnector.tool_utils(update_docstring=False, max_output_chars=None)
443
+ async def execute(entity: str, action: str, params: dict):
444
+ ...
397
445
 
398
446
  Args:
399
- func: The function to decorate
400
-
401
- Returns:
402
- The same function with updated __doc__
447
+ update_docstring: When True, append connector capabilities to __doc__.
448
+ max_output_chars: Max serialized output size before raising. Use None to disable.
403
449
  """
404
- description = generate_tool_description(SlackConnectorModel)
405
450
 
406
- original_doc = func.__doc__ or ""
407
- if original_doc.strip():
408
- func.__doc__ = f"{original_doc.strip()}\n{description}"
409
- else:
410
- func.__doc__ = description
451
+ def decorate(inner: _F) -> _F:
452
+ if update_docstring:
453
+ description = generate_tool_description(SlackConnectorModel)
454
+ original_doc = inner.__doc__ or ""
455
+ if original_doc.strip():
456
+ full_doc = f"{original_doc.strip()}\n{description}"
457
+ else:
458
+ full_doc = description
459
+ else:
460
+ full_doc = ""
461
+
462
+ if inspect.iscoroutinefunction(inner):
463
+
464
+ @wraps(inner)
465
+ async def aw(*args: Any, **kwargs: Any) -> Any:
466
+ result = await inner(*args, **kwargs)
467
+ return _check_output_size(result, max_output_chars, inner.__name__)
468
+
469
+ wrapped = aw
470
+ else:
471
+
472
+ @wraps(inner)
473
+ def sw(*args: Any, **kwargs: Any) -> Any:
474
+ result = inner(*args, **kwargs)
475
+ return _check_output_size(result, max_output_chars, inner.__name__)
476
+
477
+ wrapped = sw
478
+
479
+ if update_docstring:
480
+ wrapped.__doc__ = full_doc
481
+ return wrapped # type: ignore[return-value]
411
482
 
412
- return func
483
+ if func is not None:
484
+ return decorate(func)
485
+ return decorate
413
486
 
414
487
  def list_entities(self) -> list[dict[str, Any]]:
415
488
  """
@@ -522,6 +595,82 @@ class UsersQuery:
522
595
 
523
596
 
524
597
 
598
+ async def search(
599
+ self,
600
+ query: UsersSearchQuery,
601
+ limit: int | None = None,
602
+ cursor: str | None = None,
603
+ fields: list[list[str]] | None = None,
604
+ ) -> UsersSearchResult:
605
+ """
606
+ Search users records from Airbyte cache.
607
+
608
+ This operation searches cached data from Airbyte syncs.
609
+ Only available in hosted execution mode.
610
+
611
+ Available filter fields (UsersSearchFilter):
612
+ - color: The color assigned to the user for visual purposes.
613
+ - deleted: Indicates if the user is deleted or not.
614
+ - has_2fa: Flag indicating if the user has two-factor authentication enabled.
615
+ - id: Unique identifier for the user.
616
+ - is_admin: Flag specifying if the user is an admin or not.
617
+ - is_app_user: Specifies if the user is an app user.
618
+ - is_bot: Indicates if the user is a bot account.
619
+ - is_email_confirmed: Flag indicating if the user's email is confirmed.
620
+ - is_forgotten: Specifies if the user is marked as forgotten.
621
+ - is_invited_user: Indicates if the user is invited or not.
622
+ - is_owner: Flag indicating if the user is an owner.
623
+ - is_primary_owner: Specifies if the user is the primary owner.
624
+ - is_restricted: Flag specifying if the user is restricted.
625
+ - is_ultra_restricted: Indicates if the user has ultra-restricted access.
626
+ - name: The username of the user.
627
+ - profile: User's profile information containing detailed details.
628
+ - real_name: The real name of the user.
629
+ - team_id: Unique identifier for the team the user belongs to.
630
+ - tz: Timezone of the user.
631
+ - tz_label: Label representing the timezone of the user.
632
+ - tz_offset: Offset of the user's timezone.
633
+ - updated: Timestamp of when the user's information was last updated.
634
+ - who_can_share_contact_card: Specifies who can share the user's contact card.
635
+
636
+ Args:
637
+ query: Filter and sort conditions. Supports operators like eq, neq, gt, gte, lt, lte,
638
+ in, like, fuzzy, keyword, not, and, or. Example: {"filter": {"eq": {"status": "active"}}}
639
+ limit: Maximum results to return (default 1000)
640
+ cursor: Pagination cursor from previous response's next_cursor
641
+ fields: Field paths to include in results. Each path is a list of keys for nested access.
642
+ Example: [["id"], ["user", "name"]] returns id and user.name fields.
643
+
644
+ Returns:
645
+ UsersSearchResult with hits (list of AirbyteSearchHit[UsersSearchData]) and pagination info
646
+
647
+ Raises:
648
+ NotImplementedError: If called in local execution mode
649
+ """
650
+ params: dict[str, Any] = {"query": query}
651
+ if limit is not None:
652
+ params["limit"] = limit
653
+ if cursor is not None:
654
+ params["cursor"] = cursor
655
+ if fields is not None:
656
+ params["fields"] = fields
657
+
658
+ result = await self._connector.execute("users", "search", params)
659
+
660
+ # Parse response into typed result
661
+ return UsersSearchResult(
662
+ hits=[
663
+ AirbyteSearchHit[UsersSearchData](
664
+ id=hit.get("id"),
665
+ score=hit.get("score"),
666
+ data=UsersSearchData(**hit.get("data", {}))
667
+ )
668
+ for hit in result.get("hits", [])
669
+ ],
670
+ next_cursor=result.get("next_cursor"),
671
+ took_ms=result.get("took_ms")
672
+ )
673
+
525
674
  class ChannelsQuery:
526
675
  """
527
676
  Query class for Channels entity operations.
@@ -650,6 +799,90 @@ class ChannelsQuery:
650
799
 
651
800
 
652
801
 
802
+ async def search(
803
+ self,
804
+ query: ChannelsSearchQuery,
805
+ limit: int | None = None,
806
+ cursor: str | None = None,
807
+ fields: list[list[str]] | None = None,
808
+ ) -> ChannelsSearchResult:
809
+ """
810
+ Search channels records from Airbyte cache.
811
+
812
+ This operation searches cached data from Airbyte syncs.
813
+ Only available in hosted execution mode.
814
+
815
+ Available filter fields (ChannelsSearchFilter):
816
+ - context_team_id: The unique identifier of the team context in which the channel exists.
817
+ - created: The timestamp when the channel was created.
818
+ - creator: The ID of the user who created the channel.
819
+ - id: The unique identifier of the channel.
820
+ - is_archived: Indicates if the channel is archived.
821
+ - is_channel: Indicates if the entity is a channel.
822
+ - is_ext_shared: Indicates if the channel is externally shared.
823
+ - is_general: Indicates if the channel is a general channel in the workspace.
824
+ - is_group: Indicates if the channel is a group (private channel) rather than a regular channel.
825
+ - is_im: Indicates if the entity is a direct message (IM) channel.
826
+ - is_member: Indicates if the calling user is a member of the channel.
827
+ - is_mpim: Indicates if the entity is a multiple person direct message (MPIM) channel.
828
+ - is_org_shared: Indicates if the channel is organization-wide shared.
829
+ - is_pending_ext_shared: Indicates if the channel is pending external shared.
830
+ - is_private: Indicates if the channel is a private channel.
831
+ - is_read_only: Indicates if the channel is read-only.
832
+ - is_shared: Indicates if the channel is shared.
833
+ - last_read: The timestamp of the user's last read message in the channel.
834
+ - locale: The locale of the channel.
835
+ - name: The name of the channel.
836
+ - name_normalized: The normalized name of the channel.
837
+ - num_members: The number of members in the channel.
838
+ - parent_conversation: The parent conversation of the channel.
839
+ - pending_connected_team_ids: The IDs of teams that are pending to be connected to the channel.
840
+ - pending_shared: The list of pending shared items of the channel.
841
+ - previous_names: The previous names of the channel.
842
+ - purpose: The purpose of the channel.
843
+ - shared_team_ids: The IDs of teams with which the channel is shared.
844
+ - topic: The topic of the channel.
845
+ - unlinked: Indicates if the channel is unlinked.
846
+ - updated: The timestamp when the channel was last updated.
847
+
848
+ Args:
849
+ query: Filter and sort conditions. Supports operators like eq, neq, gt, gte, lt, lte,
850
+ in, like, fuzzy, keyword, not, and, or. Example: {"filter": {"eq": {"status": "active"}}}
851
+ limit: Maximum results to return (default 1000)
852
+ cursor: Pagination cursor from previous response's next_cursor
853
+ fields: Field paths to include in results. Each path is a list of keys for nested access.
854
+ Example: [["id"], ["user", "name"]] returns id and user.name fields.
855
+
856
+ Returns:
857
+ ChannelsSearchResult with hits (list of AirbyteSearchHit[ChannelsSearchData]) and pagination info
858
+
859
+ Raises:
860
+ NotImplementedError: If called in local execution mode
861
+ """
862
+ params: dict[str, Any] = {"query": query}
863
+ if limit is not None:
864
+ params["limit"] = limit
865
+ if cursor is not None:
866
+ params["cursor"] = cursor
867
+ if fields is not None:
868
+ params["fields"] = fields
869
+
870
+ result = await self._connector.execute("channels", "search", params)
871
+
872
+ # Parse response into typed result
873
+ return ChannelsSearchResult(
874
+ hits=[
875
+ AirbyteSearchHit[ChannelsSearchData](
876
+ id=hit.get("id"),
877
+ score=hit.get("score"),
878
+ data=ChannelsSearchData(**hit.get("data", {}))
879
+ )
880
+ for hit in result.get("hits", [])
881
+ ],
882
+ next_cursor=result.get("next_cursor"),
883
+ took_ms=result.get("took_ms")
884
+ )
885
+
653
886
  class ChannelMessagesQuery:
654
887
  """
655
888
  Query class for ChannelMessages entity operations.
@@ -27,7 +27,7 @@ from uuid import (
27
27
  SlackConnectorModel: ConnectorModel = ConnectorModel(
28
28
  id=UUID('c2281cee-86f9-4a86-bb48-d23286b4c7bd'),
29
29
  name='slack',
30
- version='0.1.5',
30
+ version='0.1.8',
31
31
  base_url='https://slack.com/api',
32
32
  auth=AuthConfig(
33
33
  options=[
@@ -38,16 +38,16 @@ SlackConnectorModel: ConnectorModel = ConnectorModel(
38
38
  user_config_spec=AirbyteAuthConfig(
39
39
  title='Token Authentication',
40
40
  type='object',
41
- required=['access_token'],
41
+ required=['api_token'],
42
42
  properties={
43
- 'access_token': AuthConfigFieldSpec(
44
- title='Access Token',
43
+ 'api_token': AuthConfigFieldSpec(
44
+ title='API Token',
45
45
  description='Your Slack Bot Token (xoxb-) or User Token (xoxp-)',
46
46
  airbyte_secret=True,
47
47
  ),
48
48
  },
49
- auth_mapping={'token': '${access_token}'},
50
- replication_auth_key_mapping={'credentials.api_token': 'access_token'},
49
+ auth_mapping={'token': '${api_token}'},
50
+ replication_auth_key_mapping={'credentials.api_token': 'api_token'},
51
51
  replication_auth_key_constants={'credentials.option_title': 'API Token Credentials'},
52
52
  ),
53
53
  ),
@@ -3192,4 +3192,104 @@ SlackConnectorModel: ConnectorModel = ConnectorModel(
3192
3192
  },
3193
3193
  ),
3194
3194
  ],
3195
+ search_field_paths={
3196
+ 'channel_members': ['channel_id', 'member_id'],
3197
+ 'channels': [
3198
+ 'context_team_id',
3199
+ 'created',
3200
+ 'creator',
3201
+ 'id',
3202
+ 'is_archived',
3203
+ 'is_channel',
3204
+ 'is_ext_shared',
3205
+ 'is_general',
3206
+ 'is_group',
3207
+ 'is_im',
3208
+ 'is_member',
3209
+ 'is_mpim',
3210
+ 'is_org_shared',
3211
+ 'is_pending_ext_shared',
3212
+ 'is_private',
3213
+ 'is_read_only',
3214
+ 'is_shared',
3215
+ 'last_read',
3216
+ 'locale',
3217
+ 'name',
3218
+ 'name_normalized',
3219
+ 'num_members',
3220
+ 'parent_conversation',
3221
+ 'pending_connected_team_ids',
3222
+ 'pending_connected_team_ids[]',
3223
+ 'pending_shared',
3224
+ 'pending_shared[]',
3225
+ 'previous_names',
3226
+ 'previous_names[]',
3227
+ 'purpose',
3228
+ 'purpose.creator',
3229
+ 'purpose.last_set',
3230
+ 'purpose.value',
3231
+ 'shared_team_ids',
3232
+ 'shared_team_ids[]',
3233
+ 'topic',
3234
+ 'topic.creator',
3235
+ 'topic.last_set',
3236
+ 'topic.value',
3237
+ 'unlinked',
3238
+ 'updated',
3239
+ ],
3240
+ 'users': [
3241
+ 'color',
3242
+ 'deleted',
3243
+ 'has_2fa',
3244
+ 'id',
3245
+ 'is_admin',
3246
+ 'is_app_user',
3247
+ 'is_bot',
3248
+ 'is_email_confirmed',
3249
+ 'is_forgotten',
3250
+ 'is_invited_user',
3251
+ 'is_owner',
3252
+ 'is_primary_owner',
3253
+ 'is_restricted',
3254
+ 'is_ultra_restricted',
3255
+ 'name',
3256
+ 'profile',
3257
+ 'profile.always_active',
3258
+ 'profile.avatar_hash',
3259
+ 'profile.display_name',
3260
+ 'profile.display_name_normalized',
3261
+ 'profile.email',
3262
+ 'profile.fields',
3263
+ 'profile.first_name',
3264
+ 'profile.huddle_state',
3265
+ 'profile.image_1024',
3266
+ 'profile.image_192',
3267
+ 'profile.image_24',
3268
+ 'profile.image_32',
3269
+ 'profile.image_48',
3270
+ 'profile.image_512',
3271
+ 'profile.image_72',
3272
+ 'profile.image_original',
3273
+ 'profile.last_name',
3274
+ 'profile.phone',
3275
+ 'profile.real_name',
3276
+ 'profile.real_name_normalized',
3277
+ 'profile.skype',
3278
+ 'profile.status_emoji',
3279
+ 'profile.status_emoji_display_info',
3280
+ 'profile.status_emoji_display_info[]',
3281
+ 'profile.status_expiration',
3282
+ 'profile.status_text',
3283
+ 'profile.status_text_canonical',
3284
+ 'profile.team',
3285
+ 'profile.title',
3286
+ 'real_name',
3287
+ 'team_id',
3288
+ 'tz',
3289
+ 'tz_label',
3290
+ 'tz_offset',
3291
+ 'updated',
3292
+ 'who_can_share_contact_card',
3293
+ ],
3294
+ },
3195
3295
  )