Skip to content

Commit 0c61cc3

Browse files
nmgarza5Nikolas Garza
andauthored
feat(slack federated search scoping - 3/4): Add connector-level config support (#6178)
Co-authored-by: Nikolas Garza <nikolas@Nikolass-MacBook-Pro.local>
1 parent 553853c commit 0c61cc3

7 files changed

Lines changed: 155 additions & 26 deletions

File tree

backend/onyx/db/federated.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,17 +63,31 @@ def create_federated_connector(
6363
db_session: Session,
6464
source: FederatedConnectorSource,
6565
credentials: dict[str, Any],
66+
config: dict[str, Any] | None = None,
6667
) -> FederatedConnector:
67-
"""Create a new federated connector with credential validation."""
68+
"""Create a new federated connector with credential and config validation."""
6869
# Validate credentials before creating
6970
if not validate_federated_connector_credentials(source, credentials):
7071
raise ValueError(
7172
f"Invalid credentials for federated connector source: {source}"
7273
)
7374

75+
# Validate config using connector-specific validation
76+
if config:
77+
try:
78+
# Get connector instance to access validate_config method
79+
connector = get_federated_connector(source, credentials)
80+
if not connector.validate_config(config):
81+
raise ValueError(
82+
f"Invalid config for federated connector source: {source}"
83+
)
84+
except Exception as e:
85+
raise ValueError(f"Config validation failed for {source}: {str(e)}")
86+
7487
federated_connector = FederatedConnector(
7588
source=source,
7689
credentials=credentials,
90+
config=config or {},
7791
)
7892
db_session.add(federated_connector)
7993
db_session.commit()
@@ -239,14 +253,21 @@ def update_federated_connector(
239253
db_session: Session,
240254
federated_connector_id: int,
241255
credentials: dict[str, Any] | None = None,
256+
config: dict[str, Any] | None = None,
242257
) -> FederatedConnector | None:
243-
"""Update a federated connector with credential validation."""
258+
"""Update a federated connector with credential and config validation."""
244259
federated_connector = fetch_federated_connector_by_id(
245260
federated_connector_id, db_session
246261
)
247262
if not federated_connector:
248263
return None
249264

265+
# Use provided credentials if updating them, otherwise use existing credentials
266+
# This is needed to instantiate the connector for config validation when only config is being updated
267+
creds_to_use = (
268+
credentials if credentials is not None else federated_connector.credentials
269+
)
270+
250271
if credentials is not None:
251272
# Validate credentials before updating
252273
if not validate_federated_connector_credentials(
@@ -257,6 +278,23 @@ def update_federated_connector(
257278
)
258279
federated_connector.credentials = credentials
259280

281+
if config is not None:
282+
# Validate config using connector-specific validation
283+
try:
284+
# Get connector instance to access validate_config method
285+
connector = get_federated_connector(
286+
federated_connector.source, creds_to_use
287+
)
288+
if not connector.validate_config(config):
289+
raise ValueError(
290+
f"Invalid config for federated connector source: {federated_connector.source}"
291+
)
292+
except Exception as e:
293+
raise ValueError(
294+
f"Config validation failed for {federated_connector.source}: {str(e)}"
295+
)
296+
federated_connector.config = config
297+
260298
db_session.commit()
261299
return federated_connector
262300

backend/onyx/federated_connectors/federated_retrieval.py

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
from collections import defaultdict
22
from collections.abc import Callable
3+
from typing import Any
34
from uuid import UUID
45

56
from pydantic import BaseModel
67
from pydantic import ConfigDict
78
from sqlalchemy.orm import Session
89

9-
from onyx.configs.app_configs import MAX_FEDERATED_CHUNKS
1010
from onyx.configs.constants import DocumentSource
1111
from onyx.configs.constants import FederatedConnectorSource
1212
from onyx.context.search.models import InferenceChunk
@@ -18,6 +18,7 @@
1818
from onyx.db.models import FederatedConnector__DocumentSet
1919
from onyx.db.slack_bot import fetch_slack_bots
2020
from onyx.federated_connectors.factory import get_federated_connector
21+
from onyx.federated_connectors.interfaces import FederatedConnector
2122
from onyx.onyxbot.slack.models import SlackContext
2223
from onyx.utils.logger import setup_logger
2324

@@ -86,15 +87,31 @@ def get_federated_retrieval_functions(
8687
credentials,
8788
)
8889

89-
federated_retrieval_infos_slack.append(
90-
FederatedRetrievalInfo(
91-
retrieval_function=lambda query: connector.search(
90+
# Capture variables by value to avoid lambda closure issues
91+
bot_token = tenant_slack_bot.bot_token
92+
93+
def create_slack_retrieval_function(
94+
conn: FederatedConnector,
95+
token: str,
96+
ctx: SlackContext,
97+
bot_tok: str,
98+
) -> Callable[[SearchQuery], list[InferenceChunk]]:
99+
def retrieval_fn(query: SearchQuery) -> list[InferenceChunk]:
100+
return conn.search(
92101
query,
93102
{}, # Empty entities for Slack context
94-
access_token=access_token,
95-
limit=MAX_FEDERATED_CHUNKS,
96-
slack_event_context=slack_context,
97-
bot_token=tenant_slack_bot.bot_token,
103+
access_token=token,
104+
limit=None, # Let connector use its own max_messages_per_query config
105+
slack_event_context=ctx,
106+
bot_token=bot_tok,
107+
)
108+
109+
return retrieval_fn
110+
111+
federated_retrieval_infos_slack.append(
112+
FederatedRetrievalInfo(
113+
retrieval_function=create_slack_retrieval_function(
114+
connector, access_token, slack_context, bot_token
98115
),
99116
source=FederatedConnectorSource.FEDERATED_SLACK,
100117
)
@@ -158,22 +175,33 @@ def get_federated_retrieval_functions(
158175
if document_set_names and not document_set_associations:
159176
continue
160177

161-
if document_set_associations:
162-
entities = document_set_associations[0].entities
163-
else:
164-
entities = {}
178+
# Only use connector-level config (no junction table entities)
179+
entities = oauth_token.federated_connector.config or {}
165180

166181
connector = get_federated_connector(
167182
oauth_token.federated_connector.source,
168183
oauth_token.federated_connector.credentials,
169184
)
185+
186+
# Capture variables by value to avoid lambda closure issues
187+
access_token = oauth_token.token
188+
189+
def create_retrieval_function(
190+
conn: FederatedConnector,
191+
ent: dict[str, Any],
192+
token: str,
193+
) -> Callable[[SearchQuery], list[InferenceChunk]]:
194+
return lambda query: conn.search(
195+
query,
196+
ent,
197+
access_token=token,
198+
limit=None, # Let connector use its own max_messages_per_query config
199+
)
200+
170201
federated_retrieval_infos.append(
171202
FederatedRetrievalInfo(
172-
retrieval_function=lambda query: connector.search(
173-
query,
174-
entities,
175-
access_token=oauth_token.token,
176-
limit=MAX_FEDERATED_CHUNKS,
203+
retrieval_function=create_retrieval_function(
204+
connector, entities, access_token
177205
),
178206
source=oauth_token.federated_connector.source,
179207
)

backend/onyx/federated_connectors/interfaces.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,34 @@ def validate_entities(self, entities: Dict[str, Any]) -> bool:
3434
3535
Returns:
3636
True if entities are valid, False otherwise
37+
38+
Note: This method is used for backward compatibility with document-set level entities.
39+
For connector-level config validation, use validate_config() instead.
40+
"""
41+
42+
def validate_config(self, config: Dict[str, Any]) -> bool:
43+
"""
44+
Validate that the provided config matches the expected structure.
45+
46+
This is an alias for validate_entities() to provide clearer semantics
47+
when validating connector-level configuration.
48+
49+
Args:
50+
config: Dictionary of configuration to validate
51+
52+
Returns:
53+
True if config is valid, False otherwise
3754
"""
55+
return self.validate_entities(config)
3856

3957
@classmethod
4058
@abstractmethod
41-
def entities_schema(cls) -> Dict[str, EntityField]:
59+
def configuration_schema(cls) -> Dict[str, EntityField]:
4260
"""
43-
Return the specification of what entities are available for this connector.
61+
Return the specification of what configuration fields are available for this connector.
4462
4563
Returns:
46-
Dictionary where keys are entity names and values are EntityField objects
64+
Dictionary where keys are configuration field names and values are EntityField objects
4765
describing the expected structure and constraints.
4866
"""
4967

@@ -96,7 +114,7 @@ def search(
96114
97115
Args:
98116
query: The search query
99-
entities: The entities to search within (validated by validate())
117+
entities: Connector-level config (entity filtering configuration)
100118
access_token: The OAuth access token
101119
limit: Maximum number of results to return
102120
slack_event_context: Slack-specific context (only used by Slack bot)

backend/onyx/federated_connectors/slack/federated_connector.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ def search(
281281
282282
Args:
283283
query: The search query
284-
entities: The entities to search within (validated by validate())
284+
entities: Connector-level config (entity filtering configuration)
285285
access_token: The OAuth access token
286286
limit: Maximum number of results to return
287287
slack_event_context: Optional Slack context for slack bot
@@ -311,9 +311,9 @@ def search(
311311
query,
312312
access_token,
313313
db_session,
314+
entities=entities,
314315
limit=limit,
315316
slack_event_context=slack_event_context,
316317
bot_token=bot_token,
317-
entities=entities,
318318
team_id=team_id,
319319
)

backend/onyx/server/federated/api.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from onyx.federated_connectors.oauth_utils import get_oauth_callback_uri
3232
from onyx.federated_connectors.oauth_utils import verify_oauth_state
3333
from onyx.server.federated.models import AuthorizeUrlResponse
34+
from onyx.server.federated.models import ConfigurationSchemaResponse
3435
from onyx.server.federated.models import CredentialSchemaResponse
3536
from onyx.server.federated.models import EntitySpecResponse
3637
from onyx.server.federated.models import FederatedConnectorCredentials
@@ -86,6 +87,7 @@ def create_federated_connector(
8687
db_session=db_session,
8788
source=federated_connector_data.source,
8889
credentials=federated_connector_data.credentials.model_dump(),
90+
config=federated_connector_data.config,
8991
)
9092

9193
logger.info(
@@ -122,7 +124,7 @@ def get_entities(
122124
connector_instance = _get_federated_connector_instance(
123125
federated_connector.source, federated_connector.credentials
124126
)
125-
entities_spec = connector_instance.entities_schema()
127+
entities_spec = connector_instance.configuration_schema()
126128

127129
# Convert EntityField objects to a dictionary format for the API response
128130
entities_dict = {}
@@ -184,6 +186,34 @@ def get_credentials_schema(
184186
raise HTTPException(status_code=500, detail=str(e))
185187

186188

189+
@router.get("/sources/{source}/configuration/schema")
190+
def get_configuration_schema_by_source(
191+
source: FederatedConnectorSource,
192+
_: User = Depends(current_curator_or_admin_user),
193+
) -> ConfigurationSchemaResponse:
194+
"""Fetch configuration schema for a specific source type (for setup/edit forms)"""
195+
try:
196+
connector_cls = get_federated_connector_cls(source)
197+
entities_spec = connector_cls.configuration_schema()
198+
199+
# Convert EntityField objects to a dictionary format for the API response
200+
configuration_dict = {}
201+
for key, field in entities_spec.items():
202+
configuration_dict[key] = {
203+
"type": field.type,
204+
"description": field.description,
205+
"required": field.required,
206+
"default": field.default,
207+
"example": field.example,
208+
}
209+
210+
return ConfigurationSchemaResponse(configuration=configuration_dict)
211+
212+
except Exception as e:
213+
logger.error(f"Error fetching configuration schema for source {source}: {e}")
214+
raise HTTPException(status_code=500, detail=str(e))
215+
216+
187217
@router.get("/sources/{source}/credentials/schema")
188218
def get_credentials_schema_by_source(
189219
source: FederatedConnectorSource,
@@ -498,6 +528,7 @@ def get_federated_connector_detail(
498528
source=federated_connector.source,
499529
name=f"{federated_connector.source.replace('_', ' ').title()}",
500530
credentials=FederatedConnectorCredentials(**federated_connector.credentials),
531+
config=federated_connector.config,
501532
oauth_token_exists=oauth_token is not None,
502533
oauth_token_expires_at=oauth_token.expires_at if oauth_token else None,
503534
document_sets=document_sets,
@@ -522,6 +553,7 @@ def update_federated_connector_endpoint(
522553
if update_request.credentials
523554
else None
524555
),
556+
config=update_request.config,
525557
)
526558

527559
if not updated_connector:

backend/onyx/server/federated/models.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class FederatedConnectorCredentials(BaseModel):
1818
class FederatedConnectorRequest(BaseModel):
1919
source: FederatedConnectorSource
2020
credentials: FederatedConnectorCredentials
21+
config: dict[str, Any] = Field(default_factory=dict)
2122

2223

2324
class FederatedConnectorResponse(BaseModel):
@@ -60,6 +61,7 @@ class FederatedConnectorDetail(BaseModel):
6061
source: FederatedConnectorSource
6162
name: str
6263
credentials: FederatedConnectorCredentials
64+
config: dict[str, Any] = Field(default_factory=dict)
6365
oauth_token_exists: bool
6466
oauth_token_expires_at: datetime | None = None
6567
document_sets: list[dict[str, Any]] = Field(default_factory=list)
@@ -87,6 +89,7 @@ def from_federated_connector_detail(
8789

8890
class FederatedConnectorUpdateRequest(BaseModel):
8991
credentials: FederatedConnectorCredentials | None = None
92+
config: dict[str, Any] | None = None
9093

9194

9295
class EntitySpecResponse(BaseModel):
@@ -95,6 +98,12 @@ class EntitySpecResponse(BaseModel):
9598
entities: dict[str, Any]
9699

97100

101+
class ConfigurationSchemaResponse(BaseModel):
102+
"""Response for configuration schema specification"""
103+
104+
configuration: dict[str, Any]
105+
106+
98107
class CredentialSchemaResponse(BaseModel):
99108
"""Response for credential schema specification"""
100109

web/src/lib/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,10 @@ export interface CredentialSchemaResponse {
560560
credentials: Record<string, CredentialFieldSpec>;
561561
}
562562

563+
export interface ConfigurationSchemaResponse {
564+
configuration: Record<string, CredentialFieldSpec>;
565+
}
566+
563567
export interface FederatedConnectorCreateRequest {
564568
source: string;
565569
credentials: Record<string, any>;

0 commit comments

Comments
 (0)