dao-ai 0.1.7__py3-none-any.whl → 0.1.9__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.
- dao_ai/app_server.py +103 -0
- dao_ai/cli.py +15 -1
- dao_ai/config.py +127 -52
- dao_ai/providers/base.py +28 -2
- dao_ai/providers/databricks.py +205 -3
- dao_ai/state.py +1 -0
- dao_ai/tools/mcp.py +60 -40
- {dao_ai-0.1.7.dist-info → dao_ai-0.1.9.dist-info}/METADATA +2 -2
- {dao_ai-0.1.7.dist-info → dao_ai-0.1.9.dist-info}/RECORD +12 -11
- {dao_ai-0.1.7.dist-info → dao_ai-0.1.9.dist-info}/WHEEL +0 -0
- {dao_ai-0.1.7.dist-info → dao_ai-0.1.9.dist-info}/entry_points.txt +0 -0
- {dao_ai-0.1.7.dist-info → dao_ai-0.1.9.dist-info}/licenses/LICENSE +0 -0
dao_ai/app_server.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""
|
|
2
|
+
App server module for running dao-ai agents as Databricks Apps.
|
|
3
|
+
|
|
4
|
+
This module provides the entry point for deploying dao-ai agents as Databricks Apps
|
|
5
|
+
using MLflow's AgentServer. It follows the same pattern as agent_as_code.py but
|
|
6
|
+
uses the AgentServer for the Databricks Apps runtime.
|
|
7
|
+
|
|
8
|
+
Configuration Loading:
|
|
9
|
+
The config path is specified via the DAO_AI_CONFIG_PATH environment variable,
|
|
10
|
+
or defaults to model_config.yaml in the current directory.
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
# With environment variable
|
|
14
|
+
DAO_AI_CONFIG_PATH=/path/to/config.yaml python -m dao_ai.app_server
|
|
15
|
+
|
|
16
|
+
# With default model_config.yaml in current directory
|
|
17
|
+
python -m dao_ai.app_server
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
from typing import AsyncGenerator
|
|
22
|
+
|
|
23
|
+
import mlflow
|
|
24
|
+
from dotenv import load_dotenv
|
|
25
|
+
from mlflow.genai.agent_server import AgentServer, invoke, stream
|
|
26
|
+
from mlflow.pyfunc import ResponsesAgent
|
|
27
|
+
from mlflow.types.responses import (
|
|
28
|
+
ResponsesAgentRequest,
|
|
29
|
+
ResponsesAgentResponse,
|
|
30
|
+
ResponsesAgentStreamEvent,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
from dao_ai.config import AppConfig
|
|
34
|
+
from dao_ai.logging import configure_logging
|
|
35
|
+
|
|
36
|
+
# Load environment variables from .env.local if it exists
|
|
37
|
+
load_dotenv(dotenv_path=".env.local", override=True)
|
|
38
|
+
|
|
39
|
+
# Configure MLflow
|
|
40
|
+
mlflow.set_registry_uri("databricks-uc")
|
|
41
|
+
mlflow.set_tracking_uri("databricks")
|
|
42
|
+
mlflow.langchain.autolog()
|
|
43
|
+
|
|
44
|
+
# Get config path from environment or use default
|
|
45
|
+
config_path: str = os.environ.get("DAO_AI_CONFIG_PATH", "model_config.yaml")
|
|
46
|
+
|
|
47
|
+
# Load configuration using AppConfig.from_file (consistent with CLI, notebook, builder)
|
|
48
|
+
config: AppConfig = AppConfig.from_file(config_path)
|
|
49
|
+
|
|
50
|
+
# Configure logging
|
|
51
|
+
if config.app and config.app.log_level:
|
|
52
|
+
configure_logging(level=config.app.log_level)
|
|
53
|
+
|
|
54
|
+
# Create the ResponsesAgent
|
|
55
|
+
_responses_agent: ResponsesAgent = config.as_responses_agent()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@invoke()
|
|
59
|
+
def non_streaming(request: ResponsesAgentRequest) -> ResponsesAgentResponse:
|
|
60
|
+
"""
|
|
61
|
+
Handle non-streaming requests by delegating to the ResponsesAgent.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
request: The incoming ResponsesAgentRequest
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
ResponsesAgentResponse with the complete output
|
|
68
|
+
"""
|
|
69
|
+
return _responses_agent.predict(request)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@stream()
|
|
73
|
+
def streaming(
|
|
74
|
+
request: ResponsesAgentRequest,
|
|
75
|
+
) -> AsyncGenerator[ResponsesAgentStreamEvent, None]:
|
|
76
|
+
"""
|
|
77
|
+
Handle streaming requests by delegating to the ResponsesAgent.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
request: The incoming ResponsesAgentRequest
|
|
81
|
+
|
|
82
|
+
Yields:
|
|
83
|
+
ResponsesAgentStreamEvent objects as they are generated
|
|
84
|
+
"""
|
|
85
|
+
# The predict_stream method returns a generator, convert to async generator
|
|
86
|
+
for event in _responses_agent.predict_stream(request):
|
|
87
|
+
yield event
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# Create the AgentServer instance
|
|
91
|
+
agent_server = AgentServer("ResponsesAgent", enable_chat_proxy=True)
|
|
92
|
+
|
|
93
|
+
# Define the app as a module level variable to enable multiple workers
|
|
94
|
+
app = agent_server.app
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def main() -> None:
|
|
98
|
+
"""Entry point for running the agent server."""
|
|
99
|
+
agent_server.run(app_import_string="dao_ai.app_server:app")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
if __name__ == "__main__":
|
|
103
|
+
main()
|
dao_ai/cli.py
CHANGED
|
@@ -309,6 +309,14 @@ Examples:
|
|
|
309
309
|
metavar="FILE",
|
|
310
310
|
help="Path to the model configuration file to validate",
|
|
311
311
|
)
|
|
312
|
+
deploy_parser.add_argument(
|
|
313
|
+
"-t",
|
|
314
|
+
"--target",
|
|
315
|
+
type=str,
|
|
316
|
+
choices=["model_serving", "apps"],
|
|
317
|
+
default="model_serving",
|
|
318
|
+
help="Deployment target: 'model_serving' (default) or 'apps'",
|
|
319
|
+
)
|
|
312
320
|
|
|
313
321
|
# List MCP tools command
|
|
314
322
|
list_mcp_parser: ArgumentParser = subparsers.add_parser(
|
|
@@ -729,11 +737,17 @@ def handle_graph_command(options: Namespace) -> None:
|
|
|
729
737
|
|
|
730
738
|
|
|
731
739
|
def handle_deploy_command(options: Namespace) -> None:
|
|
740
|
+
from dao_ai.config import DeploymentTarget
|
|
741
|
+
|
|
732
742
|
logger.debug(f"Validating configuration from {options.config}...")
|
|
733
743
|
try:
|
|
734
744
|
config: AppConfig = AppConfig.from_file(options.config)
|
|
745
|
+
|
|
746
|
+
# Convert target string to enum
|
|
747
|
+
target: DeploymentTarget = DeploymentTarget(options.target)
|
|
748
|
+
|
|
735
749
|
config.create_agent()
|
|
736
|
-
config.deploy_agent()
|
|
750
|
+
config.deploy_agent(target=target)
|
|
737
751
|
sys.exit(0)
|
|
738
752
|
except Exception as e:
|
|
739
753
|
logger.error(f"Deployment failed: {e}")
|
dao_ai/config.py
CHANGED
|
@@ -208,7 +208,9 @@ class IsDatabricksResource(ABC, BaseModel):
|
|
|
208
208
|
Authentication Options:
|
|
209
209
|
----------------------
|
|
210
210
|
1. **On-Behalf-Of User (OBO)**: Set on_behalf_of_user=True to use the
|
|
211
|
-
calling user's identity
|
|
211
|
+
calling user's identity. Implementation varies by deployment:
|
|
212
|
+
- Databricks Apps: Uses X-Forwarded-Access-Token from request headers
|
|
213
|
+
- Model Serving: Uses ModelServingUserCredentials
|
|
212
214
|
|
|
213
215
|
2. **Service Principal (OAuth M2M)**: Provide service_principal or
|
|
214
216
|
(client_id + client_secret + workspace_host) for service principal auth.
|
|
@@ -221,9 +223,17 @@ class IsDatabricksResource(ABC, BaseModel):
|
|
|
221
223
|
|
|
222
224
|
Authentication Priority:
|
|
223
225
|
1. OBO (on_behalf_of_user=True)
|
|
226
|
+
- Checks for forwarded headers (Databricks Apps)
|
|
227
|
+
- Falls back to ModelServingUserCredentials (Model Serving)
|
|
224
228
|
2. Service Principal (client_id + client_secret + workspace_host)
|
|
225
229
|
3. PAT (pat + workspace_host)
|
|
226
230
|
4. Ambient/default authentication
|
|
231
|
+
|
|
232
|
+
Note: When on_behalf_of_user=True, the agent acts as the calling user regardless
|
|
233
|
+
of deployment target. In Databricks Apps, this uses X-Forwarded-Access-Token
|
|
234
|
+
automatically captured by MLflow AgentServer. In Model Serving, this uses
|
|
235
|
+
ModelServingUserCredentials. Forwarded headers are ONLY used when
|
|
236
|
+
on_behalf_of_user=True.
|
|
227
237
|
"""
|
|
228
238
|
|
|
229
239
|
model_config = ConfigDict(use_enum_values=True)
|
|
@@ -235,9 +245,6 @@ class IsDatabricksResource(ABC, BaseModel):
|
|
|
235
245
|
workspace_host: Optional[AnyVariable] = None
|
|
236
246
|
pat: Optional[AnyVariable] = None
|
|
237
247
|
|
|
238
|
-
# Private attribute to cache the workspace client (lazy instantiation)
|
|
239
|
-
_workspace_client: Optional[WorkspaceClient] = PrivateAttr(default=None)
|
|
240
|
-
|
|
241
248
|
@abstractmethod
|
|
242
249
|
def as_resources(self) -> Sequence[DatabricksResource]: ...
|
|
243
250
|
|
|
@@ -273,32 +280,56 @@ class IsDatabricksResource(ABC, BaseModel):
|
|
|
273
280
|
"""
|
|
274
281
|
Get a WorkspaceClient configured with the appropriate authentication.
|
|
275
282
|
|
|
276
|
-
|
|
283
|
+
A new client is created on each access.
|
|
277
284
|
|
|
278
285
|
Authentication priority:
|
|
279
|
-
1.
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
286
|
+
1. On-Behalf-Of User (on_behalf_of_user=True):
|
|
287
|
+
- Forwarded headers (Databricks Apps)
|
|
288
|
+
- ModelServingUserCredentials (Model Serving)
|
|
289
|
+
2. Service Principal (client_id + client_secret + workspace_host)
|
|
290
|
+
3. PAT (pat + workspace_host)
|
|
291
|
+
4. Ambient/default authentication
|
|
284
292
|
"""
|
|
285
|
-
# Return cached client if already instantiated
|
|
286
|
-
if self._workspace_client is not None:
|
|
287
|
-
return self._workspace_client
|
|
288
|
-
|
|
289
293
|
from dao_ai.utils import normalize_host
|
|
290
294
|
|
|
291
295
|
# Check for OBO first (highest priority)
|
|
292
296
|
if self.on_behalf_of_user:
|
|
297
|
+
# NEW: In Databricks Apps, use forwarded headers for per-user auth
|
|
298
|
+
try:
|
|
299
|
+
from mlflow.genai.agent_server import get_request_headers
|
|
300
|
+
|
|
301
|
+
headers = get_request_headers()
|
|
302
|
+
forwarded_token = headers.get("x-forwarded-access-token")
|
|
303
|
+
|
|
304
|
+
if forwarded_token:
|
|
305
|
+
forwarded_user = headers.get("x-forwarded-user", "unknown")
|
|
306
|
+
logger.debug(
|
|
307
|
+
f"Creating WorkspaceClient for {self.__class__.__name__} "
|
|
308
|
+
f"with OBO using forwarded token from Databricks Apps",
|
|
309
|
+
forwarded_user=forwarded_user,
|
|
310
|
+
)
|
|
311
|
+
# Use workspace_host if configured, otherwise SDK will auto-detect
|
|
312
|
+
workspace_host_value: str | None = (
|
|
313
|
+
normalize_host(value_of(self.workspace_host))
|
|
314
|
+
if self.workspace_host
|
|
315
|
+
else None
|
|
316
|
+
)
|
|
317
|
+
return WorkspaceClient(
|
|
318
|
+
host=workspace_host_value,
|
|
319
|
+
token=forwarded_token,
|
|
320
|
+
auth_type="pat",
|
|
321
|
+
)
|
|
322
|
+
except (ImportError, LookupError):
|
|
323
|
+
# mlflow not available or headers not set - fall through to Model Serving
|
|
324
|
+
pass
|
|
325
|
+
|
|
326
|
+
# Fall back to Model Serving OBO (existing behavior)
|
|
293
327
|
credentials_strategy: CredentialsStrategy = ModelServingUserCredentials()
|
|
294
328
|
logger.debug(
|
|
295
329
|
f"Creating WorkspaceClient for {self.__class__.__name__} "
|
|
296
|
-
f"with OBO credentials strategy"
|
|
297
|
-
)
|
|
298
|
-
self._workspace_client = WorkspaceClient(
|
|
299
|
-
credentials_strategy=credentials_strategy
|
|
330
|
+
f"with OBO credentials strategy (Model Serving)"
|
|
300
331
|
)
|
|
301
|
-
return
|
|
332
|
+
return WorkspaceClient(credentials_strategy=credentials_strategy)
|
|
302
333
|
|
|
303
334
|
# Check for service principal credentials
|
|
304
335
|
client_id_value: str | None = (
|
|
@@ -318,13 +349,12 @@ class IsDatabricksResource(ABC, BaseModel):
|
|
|
318
349
|
f"Creating WorkspaceClient for {self.__class__.__name__} with service principal: "
|
|
319
350
|
f"client_id={client_id_value}, host={workspace_host_value}"
|
|
320
351
|
)
|
|
321
|
-
|
|
352
|
+
return WorkspaceClient(
|
|
322
353
|
host=workspace_host_value,
|
|
323
354
|
client_id=client_id_value,
|
|
324
355
|
client_secret=client_secret_value,
|
|
325
356
|
auth_type="oauth-m2m",
|
|
326
357
|
)
|
|
327
|
-
return self._workspace_client
|
|
328
358
|
|
|
329
359
|
# Check for PAT authentication
|
|
330
360
|
pat_value: str | None = value_of(self.pat) if self.pat else None
|
|
@@ -332,20 +362,28 @@ class IsDatabricksResource(ABC, BaseModel):
|
|
|
332
362
|
logger.debug(
|
|
333
363
|
f"Creating WorkspaceClient for {self.__class__.__name__} with PAT"
|
|
334
364
|
)
|
|
335
|
-
|
|
365
|
+
return WorkspaceClient(
|
|
336
366
|
host=workspace_host_value,
|
|
337
367
|
token=pat_value,
|
|
338
368
|
auth_type="pat",
|
|
339
369
|
)
|
|
340
|
-
return self._workspace_client
|
|
341
370
|
|
|
342
371
|
# Default: use ambient authentication
|
|
343
372
|
logger.debug(
|
|
344
373
|
f"Creating WorkspaceClient for {self.__class__.__name__} "
|
|
345
374
|
"with default/ambient authentication"
|
|
346
375
|
)
|
|
347
|
-
|
|
348
|
-
|
|
376
|
+
return WorkspaceClient()
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
class DeploymentTarget(str, Enum):
|
|
380
|
+
"""Target platform for agent deployment."""
|
|
381
|
+
|
|
382
|
+
MODEL_SERVING = "model_serving"
|
|
383
|
+
"""Deploy to Databricks Model Serving endpoint."""
|
|
384
|
+
|
|
385
|
+
APPS = "apps"
|
|
386
|
+
"""Deploy as a Databricks App."""
|
|
349
387
|
|
|
350
388
|
|
|
351
389
|
class Privilege(str, Enum):
|
|
@@ -418,11 +456,11 @@ class SchemaModel(BaseModel, HasFullName):
|
|
|
418
456
|
class DatabricksAppModel(IsDatabricksResource, HasFullName):
|
|
419
457
|
"""
|
|
420
458
|
Configuration for a Databricks App resource.
|
|
421
|
-
|
|
459
|
+
|
|
422
460
|
The `name` is the unique instance name of the Databricks App within the workspace.
|
|
423
|
-
The `url` is dynamically retrieved from the workspace client by calling
|
|
461
|
+
The `url` is dynamically retrieved from the workspace client by calling
|
|
424
462
|
`apps.get(name)` and returning the app's URL.
|
|
425
|
-
|
|
463
|
+
|
|
426
464
|
Example:
|
|
427
465
|
```yaml
|
|
428
466
|
resources:
|
|
@@ -431,7 +469,7 @@ class DatabricksAppModel(IsDatabricksResource, HasFullName):
|
|
|
431
469
|
name: my-databricks-app
|
|
432
470
|
```
|
|
433
471
|
"""
|
|
434
|
-
|
|
472
|
+
|
|
435
473
|
model_config = ConfigDict(use_enum_values=True, extra="forbid")
|
|
436
474
|
name: str
|
|
437
475
|
"""The unique instance name of the Databricks App in the workspace."""
|
|
@@ -440,10 +478,10 @@ class DatabricksAppModel(IsDatabricksResource, HasFullName):
|
|
|
440
478
|
def url(self) -> str:
|
|
441
479
|
"""
|
|
442
480
|
Retrieve the URL of the Databricks App from the workspace.
|
|
443
|
-
|
|
481
|
+
|
|
444
482
|
Returns:
|
|
445
483
|
The URL of the deployed Databricks App.
|
|
446
|
-
|
|
484
|
+
|
|
447
485
|
Raises:
|
|
448
486
|
RuntimeError: If the app is not found or URL is not available.
|
|
449
487
|
"""
|
|
@@ -455,7 +493,6 @@ class DatabricksAppModel(IsDatabricksResource, HasFullName):
|
|
|
455
493
|
)
|
|
456
494
|
return app.url
|
|
457
495
|
|
|
458
|
-
|
|
459
496
|
@property
|
|
460
497
|
def full_name(self) -> str:
|
|
461
498
|
return self.name
|
|
@@ -761,11 +798,20 @@ class FunctionModel(IsDatabricksResource, HasFullName):
|
|
|
761
798
|
|
|
762
799
|
|
|
763
800
|
class WarehouseModel(IsDatabricksResource):
|
|
764
|
-
model_config = ConfigDict()
|
|
765
|
-
name: str
|
|
801
|
+
model_config = ConfigDict(use_enum_values=True, extra="forbid")
|
|
802
|
+
name: Optional[str] = None
|
|
766
803
|
description: Optional[str] = None
|
|
767
804
|
warehouse_id: AnyVariable
|
|
768
805
|
|
|
806
|
+
_warehouse_details: Optional[GetWarehouseResponse] = PrivateAttr(default=None)
|
|
807
|
+
|
|
808
|
+
def _get_warehouse_details(self) -> GetWarehouseResponse:
|
|
809
|
+
if self._warehouse_details is None:
|
|
810
|
+
self._warehouse_details = self.workspace_client.warehouses.get(
|
|
811
|
+
id=value_of(self.warehouse_id)
|
|
812
|
+
)
|
|
813
|
+
return self._warehouse_details
|
|
814
|
+
|
|
769
815
|
@property
|
|
770
816
|
def api_scopes(self) -> Sequence[str]:
|
|
771
817
|
return [
|
|
@@ -786,10 +832,22 @@ class WarehouseModel(IsDatabricksResource):
|
|
|
786
832
|
self.warehouse_id = value_of(self.warehouse_id)
|
|
787
833
|
return self
|
|
788
834
|
|
|
835
|
+
@model_validator(mode="after")
|
|
836
|
+
def populate_name(self) -> Self:
|
|
837
|
+
"""Populate name from warehouse details if not provided."""
|
|
838
|
+
if self.warehouse_id and not self.name:
|
|
839
|
+
try:
|
|
840
|
+
warehouse_details = self._get_warehouse_details()
|
|
841
|
+
if warehouse_details.name:
|
|
842
|
+
self.name = warehouse_details.name
|
|
843
|
+
except Exception as e:
|
|
844
|
+
logger.debug(f"Could not fetch details from warehouse: {e}")
|
|
845
|
+
return self
|
|
846
|
+
|
|
789
847
|
|
|
790
848
|
class GenieRoomModel(IsDatabricksResource):
|
|
791
849
|
model_config = ConfigDict(use_enum_values=True, extra="forbid")
|
|
792
|
-
name: str
|
|
850
|
+
name: Optional[str] = None
|
|
793
851
|
description: Optional[str] = None
|
|
794
852
|
space_id: AnyVariable
|
|
795
853
|
|
|
@@ -845,10 +903,6 @@ class GenieRoomModel(IsDatabricksResource):
|
|
|
845
903
|
pat=self.pat,
|
|
846
904
|
)
|
|
847
905
|
|
|
848
|
-
# Share the cached workspace client if available
|
|
849
|
-
if self._workspace_client is not None:
|
|
850
|
-
warehouse_model._workspace_client = self._workspace_client
|
|
851
|
-
|
|
852
906
|
return warehouse_model
|
|
853
907
|
except Exception as e:
|
|
854
908
|
logger.warning(
|
|
@@ -892,9 +946,6 @@ class GenieRoomModel(IsDatabricksResource):
|
|
|
892
946
|
workspace_host=self.workspace_host,
|
|
893
947
|
pat=self.pat,
|
|
894
948
|
)
|
|
895
|
-
# Share the cached workspace client if available
|
|
896
|
-
if self._workspace_client is not None:
|
|
897
|
-
table_model._workspace_client = self._workspace_client
|
|
898
949
|
|
|
899
950
|
# Verify the table exists before adding
|
|
900
951
|
if not table_model.exists():
|
|
@@ -932,9 +983,6 @@ class GenieRoomModel(IsDatabricksResource):
|
|
|
932
983
|
workspace_host=self.workspace_host,
|
|
933
984
|
pat=self.pat,
|
|
934
985
|
)
|
|
935
|
-
# Share the cached workspace client if available
|
|
936
|
-
if self._workspace_client is not None:
|
|
937
|
-
function_model._workspace_client = self._workspace_client
|
|
938
986
|
|
|
939
987
|
# Verify the function exists before adding
|
|
940
988
|
if not function_model.exists():
|
|
@@ -998,15 +1046,17 @@ class GenieRoomModel(IsDatabricksResource):
|
|
|
998
1046
|
return self
|
|
999
1047
|
|
|
1000
1048
|
@model_validator(mode="after")
|
|
1001
|
-
def
|
|
1002
|
-
"""Populate description from GenieSpace if not provided."""
|
|
1003
|
-
if not self.description:
|
|
1049
|
+
def populate_name_and_description(self) -> Self:
|
|
1050
|
+
"""Populate name and description from GenieSpace if not provided."""
|
|
1051
|
+
if self.space_id and (not self.name or not self.description):
|
|
1004
1052
|
try:
|
|
1005
1053
|
space_details = self._get_space_details()
|
|
1006
|
-
if space_details.
|
|
1054
|
+
if not self.name and space_details.title:
|
|
1055
|
+
self.name = space_details.title
|
|
1056
|
+
if not self.description and space_details.description:
|
|
1007
1057
|
self.description = space_details.description
|
|
1008
1058
|
except Exception as e:
|
|
1009
|
-
logger.debug(f"Could not fetch
|
|
1059
|
+
logger.debug(f"Could not fetch details from Genie space: {e}")
|
|
1010
1060
|
return self
|
|
1011
1061
|
|
|
1012
1062
|
|
|
@@ -2007,7 +2057,7 @@ class McpFunctionModel(BaseFunctionModel, IsDatabricksResource):
|
|
|
2007
2057
|
# DBSQL MCP server (serverless, workspace-level)
|
|
2008
2058
|
if self.sql:
|
|
2009
2059
|
return f"{workspace_host}/api/2.0/mcp/sql"
|
|
2010
|
-
|
|
2060
|
+
|
|
2011
2061
|
# Databricks App
|
|
2012
2062
|
if self.app:
|
|
2013
2063
|
return self.app.url
|
|
@@ -3233,6 +3283,7 @@ class ResourcesModel(BaseModel):
|
|
|
3233
3283
|
|
|
3234
3284
|
class AppConfig(BaseModel):
|
|
3235
3285
|
model_config = ConfigDict(use_enum_values=True, extra="forbid")
|
|
3286
|
+
version: Optional[str] = None
|
|
3236
3287
|
variables: dict[str, AnyVariable] = Field(default_factory=dict)
|
|
3237
3288
|
service_principals: dict[str, ServicePrincipalModel] = Field(default_factory=dict)
|
|
3238
3289
|
schemas: dict[str, SchemaModel] = Field(default_factory=dict)
|
|
@@ -3253,6 +3304,9 @@ class AppConfig(BaseModel):
|
|
|
3253
3304
|
)
|
|
3254
3305
|
providers: Optional[dict[type | str, Any]] = None
|
|
3255
3306
|
|
|
3307
|
+
# Private attribute to track the source config file path (set by from_file)
|
|
3308
|
+
_source_config_path: str | None = None
|
|
3309
|
+
|
|
3256
3310
|
@classmethod
|
|
3257
3311
|
def from_file(cls, path: PathLike) -> "AppConfig":
|
|
3258
3312
|
path = Path(path).as_posix()
|
|
@@ -3260,12 +3314,20 @@ class AppConfig(BaseModel):
|
|
|
3260
3314
|
model_config: ModelConfig = ModelConfig(development_config=path)
|
|
3261
3315
|
config: AppConfig = AppConfig(**model_config.to_dict())
|
|
3262
3316
|
|
|
3317
|
+
# Store the source config path for later use (e.g., Apps deployment)
|
|
3318
|
+
config._source_config_path = path
|
|
3319
|
+
|
|
3263
3320
|
config.initialize()
|
|
3264
3321
|
|
|
3265
3322
|
atexit.register(config.shutdown)
|
|
3266
3323
|
|
|
3267
3324
|
return config
|
|
3268
3325
|
|
|
3326
|
+
@property
|
|
3327
|
+
def source_config_path(self) -> str | None:
|
|
3328
|
+
"""Get the source config file path if loaded via from_file."""
|
|
3329
|
+
return self._source_config_path
|
|
3330
|
+
|
|
3269
3331
|
def initialize(self) -> None:
|
|
3270
3332
|
from dao_ai.hooks.core import create_hooks
|
|
3271
3333
|
from dao_ai.logging import configure_logging
|
|
@@ -3336,6 +3398,7 @@ class AppConfig(BaseModel):
|
|
|
3336
3398
|
|
|
3337
3399
|
def deploy_agent(
|
|
3338
3400
|
self,
|
|
3401
|
+
target: DeploymentTarget = DeploymentTarget.MODEL_SERVING,
|
|
3339
3402
|
w: WorkspaceClient | None = None,
|
|
3340
3403
|
vsc: "VectorSearchClient | None" = None,
|
|
3341
3404
|
pat: str | None = None,
|
|
@@ -3343,6 +3406,18 @@ class AppConfig(BaseModel):
|
|
|
3343
3406
|
client_secret: str | None = None,
|
|
3344
3407
|
workspace_host: str | None = None,
|
|
3345
3408
|
) -> None:
|
|
3409
|
+
"""
|
|
3410
|
+
Deploy the agent to the specified target.
|
|
3411
|
+
|
|
3412
|
+
Args:
|
|
3413
|
+
target: The deployment target (MODEL_SERVING or APPS). Defaults to MODEL_SERVING.
|
|
3414
|
+
w: Optional WorkspaceClient instance
|
|
3415
|
+
vsc: Optional VectorSearchClient instance
|
|
3416
|
+
pat: Optional personal access token for authentication
|
|
3417
|
+
client_id: Optional client ID for service principal authentication
|
|
3418
|
+
client_secret: Optional client secret for service principal authentication
|
|
3419
|
+
workspace_host: Optional workspace host URL
|
|
3420
|
+
"""
|
|
3346
3421
|
from dao_ai.providers.base import ServiceProvider
|
|
3347
3422
|
from dao_ai.providers.databricks import DatabricksProvider
|
|
3348
3423
|
|
|
@@ -3354,7 +3429,7 @@ class AppConfig(BaseModel):
|
|
|
3354
3429
|
client_secret=client_secret,
|
|
3355
3430
|
workspace_host=workspace_host,
|
|
3356
3431
|
)
|
|
3357
|
-
provider.deploy_agent(self)
|
|
3432
|
+
provider.deploy_agent(self, target=target)
|
|
3358
3433
|
|
|
3359
3434
|
def find_agents(
|
|
3360
3435
|
self, predicate: Callable[[AgentModel], bool] | None = None
|
dao_ai/providers/base.py
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
|
-
from typing import Any, Sequence
|
|
2
|
+
from typing import TYPE_CHECKING, Any, Sequence
|
|
3
3
|
|
|
4
4
|
from dao_ai.config import (
|
|
5
5
|
AppModel,
|
|
6
6
|
DatasetModel,
|
|
7
|
+
DeploymentTarget,
|
|
7
8
|
SchemaModel,
|
|
8
9
|
UnityCatalogFunctionSqlModel,
|
|
9
10
|
VectorStoreModel,
|
|
10
11
|
VolumeModel,
|
|
11
12
|
)
|
|
12
13
|
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from dao_ai.config import AppConfig
|
|
16
|
+
|
|
13
17
|
|
|
14
18
|
class ServiceProvider(ABC):
|
|
15
19
|
@abstractmethod
|
|
@@ -52,4 +56,26 @@ class ServiceProvider(ABC):
|
|
|
52
56
|
) -> Any: ...
|
|
53
57
|
|
|
54
58
|
@abstractmethod
|
|
55
|
-
def
|
|
59
|
+
def deploy_model_serving_agent(self, config: "AppConfig") -> Any:
|
|
60
|
+
"""Deploy agent to Databricks Model Serving endpoint."""
|
|
61
|
+
...
|
|
62
|
+
|
|
63
|
+
@abstractmethod
|
|
64
|
+
def deploy_apps_agent(self, config: "AppConfig") -> Any:
|
|
65
|
+
"""Deploy agent as a Databricks App."""
|
|
66
|
+
...
|
|
67
|
+
|
|
68
|
+
@abstractmethod
|
|
69
|
+
def deploy_agent(
|
|
70
|
+
self,
|
|
71
|
+
config: "AppConfig",
|
|
72
|
+
target: DeploymentTarget = DeploymentTarget.MODEL_SERVING,
|
|
73
|
+
) -> Any:
|
|
74
|
+
"""
|
|
75
|
+
Deploy agent to the specified target.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
config: The AppConfig containing deployment configuration
|
|
79
|
+
target: The deployment target (MODEL_SERVING or APPS)
|
|
80
|
+
"""
|
|
81
|
+
...
|
dao_ai/providers/databricks.py
CHANGED
|
@@ -23,7 +23,7 @@ from databricks.sdk.service.catalog import (
|
|
|
23
23
|
)
|
|
24
24
|
from databricks.sdk.service.database import DatabaseCredential
|
|
25
25
|
from databricks.sdk.service.iam import User
|
|
26
|
-
from databricks.sdk.service.workspace import GetSecretResponse
|
|
26
|
+
from databricks.sdk.service.workspace import GetSecretResponse, ImportFormat
|
|
27
27
|
from databricks.vector_search.client import VectorSearchClient
|
|
28
28
|
from databricks.vector_search.index import VectorSearchIndex
|
|
29
29
|
from loguru import logger
|
|
@@ -48,6 +48,7 @@ from dao_ai.config import (
|
|
|
48
48
|
DatabaseModel,
|
|
49
49
|
DatabricksAppModel,
|
|
50
50
|
DatasetModel,
|
|
51
|
+
DeploymentTarget,
|
|
51
52
|
FunctionModel,
|
|
52
53
|
GenieRoomModel,
|
|
53
54
|
HasFullName,
|
|
@@ -439,8 +440,19 @@ class DatabricksProvider(ServiceProvider):
|
|
|
439
440
|
version=aliased_model.version,
|
|
440
441
|
)
|
|
441
442
|
|
|
442
|
-
def
|
|
443
|
-
|
|
443
|
+
def deploy_model_serving_agent(self, config: AppConfig) -> None:
|
|
444
|
+
"""
|
|
445
|
+
Deploy agent to Databricks Model Serving endpoint.
|
|
446
|
+
|
|
447
|
+
This is the original deployment method that creates/updates a Model Serving
|
|
448
|
+
endpoint with the registered model.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
config: The AppConfig containing deployment configuration
|
|
452
|
+
"""
|
|
453
|
+
logger.info(
|
|
454
|
+
"Deploying agent to Model Serving", endpoint_name=config.app.endpoint_name
|
|
455
|
+
)
|
|
444
456
|
mlflow.set_registry_uri("databricks-uc")
|
|
445
457
|
|
|
446
458
|
endpoint_name: str = config.app.endpoint_name
|
|
@@ -499,6 +511,196 @@ class DatabricksProvider(ServiceProvider):
|
|
|
499
511
|
permission_level=PermissionLevel[entitlement],
|
|
500
512
|
)
|
|
501
513
|
|
|
514
|
+
def deploy_apps_agent(self, config: AppConfig) -> None:
|
|
515
|
+
"""
|
|
516
|
+
Deploy agent as a Databricks App.
|
|
517
|
+
|
|
518
|
+
This method creates or updates a Databricks App that serves the agent
|
|
519
|
+
using the app_server module.
|
|
520
|
+
|
|
521
|
+
The deployment process:
|
|
522
|
+
1. Determine the workspace source path for the app
|
|
523
|
+
2. Upload the configuration file to the workspace
|
|
524
|
+
3. Create the app if it doesn't exist
|
|
525
|
+
4. Deploy the app
|
|
526
|
+
|
|
527
|
+
Args:
|
|
528
|
+
config: The AppConfig containing deployment configuration
|
|
529
|
+
|
|
530
|
+
Note:
|
|
531
|
+
The config file must be loaded via AppConfig.from_file() so that
|
|
532
|
+
the source_config_path is available for upload.
|
|
533
|
+
"""
|
|
534
|
+
import io
|
|
535
|
+
|
|
536
|
+
from databricks.sdk.service.apps import (
|
|
537
|
+
App,
|
|
538
|
+
AppDeployment,
|
|
539
|
+
AppDeploymentMode,
|
|
540
|
+
AppDeploymentState,
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
# Normalize app name: lowercase, replace underscores with dashes
|
|
544
|
+
raw_name: str = config.app.name
|
|
545
|
+
app_name: str = raw_name.lower().replace("_", "-")
|
|
546
|
+
if app_name != raw_name:
|
|
547
|
+
logger.info(
|
|
548
|
+
"Normalized app name for Databricks Apps",
|
|
549
|
+
original=raw_name,
|
|
550
|
+
normalized=app_name,
|
|
551
|
+
)
|
|
552
|
+
logger.info("Deploying agent to Databricks Apps", app_name=app_name)
|
|
553
|
+
|
|
554
|
+
# Use convention-based workspace path: /Workspace/Users/{user}/apps/{app_name}
|
|
555
|
+
current_user: User = self.w.current_user.me()
|
|
556
|
+
user_name: str = current_user.user_name or "default"
|
|
557
|
+
source_path: str = f"/Workspace/Users/{user_name}/apps/{app_name}"
|
|
558
|
+
|
|
559
|
+
logger.info("Using workspace source path", source_path=source_path)
|
|
560
|
+
|
|
561
|
+
# Upload the configuration file to the workspace
|
|
562
|
+
source_config_path: str | None = config.source_config_path
|
|
563
|
+
if source_config_path:
|
|
564
|
+
# Read the config file and upload to workspace
|
|
565
|
+
config_file_name: str = "model_config.yaml"
|
|
566
|
+
workspace_config_path: str = f"{source_path}/{config_file_name}"
|
|
567
|
+
|
|
568
|
+
logger.info(
|
|
569
|
+
"Uploading config file to workspace",
|
|
570
|
+
source=source_config_path,
|
|
571
|
+
destination=workspace_config_path,
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
# Read the source config file
|
|
575
|
+
with open(source_config_path, "rb") as f:
|
|
576
|
+
config_content: bytes = f.read()
|
|
577
|
+
|
|
578
|
+
# Create the directory if it doesn't exist and upload the file
|
|
579
|
+
try:
|
|
580
|
+
self.w.workspace.mkdirs(source_path)
|
|
581
|
+
except Exception as e:
|
|
582
|
+
logger.debug(f"Directory may already exist: {e}")
|
|
583
|
+
|
|
584
|
+
# Upload the config file
|
|
585
|
+
self.w.workspace.upload(
|
|
586
|
+
path=workspace_config_path,
|
|
587
|
+
content=io.BytesIO(config_content),
|
|
588
|
+
format=ImportFormat.AUTO,
|
|
589
|
+
overwrite=True,
|
|
590
|
+
)
|
|
591
|
+
logger.info("Config file uploaded", path=workspace_config_path)
|
|
592
|
+
else:
|
|
593
|
+
logger.warning(
|
|
594
|
+
"No source config path available. "
|
|
595
|
+
"Ensure DAO_AI_CONFIG_PATH is set in the app environment or "
|
|
596
|
+
"model_config.yaml exists in the app source directory."
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
# Generate and upload app.yaml
|
|
600
|
+
# Use pip to install dao-ai and run the app server
|
|
601
|
+
app_yaml_content: str = """command:
|
|
602
|
+
- /bin/bash
|
|
603
|
+
- -c
|
|
604
|
+
- |
|
|
605
|
+
pip install dao-ai && python -m dao_ai.app_server
|
|
606
|
+
|
|
607
|
+
env:
|
|
608
|
+
- name: MLFLOW_TRACKING_URI
|
|
609
|
+
value: "databricks"
|
|
610
|
+
- name: MLFLOW_REGISTRY_URI
|
|
611
|
+
value: "databricks-uc"
|
|
612
|
+
- name: DAO_AI_CONFIG_PATH
|
|
613
|
+
value: "model_config.yaml"
|
|
614
|
+
"""
|
|
615
|
+
app_yaml_path: str = f"{source_path}/app.yaml"
|
|
616
|
+
self.w.workspace.upload(
|
|
617
|
+
path=app_yaml_path,
|
|
618
|
+
content=io.BytesIO(app_yaml_content.encode("utf-8")),
|
|
619
|
+
format=ImportFormat.AUTO,
|
|
620
|
+
overwrite=True,
|
|
621
|
+
)
|
|
622
|
+
logger.info("app.yaml uploaded", path=app_yaml_path)
|
|
623
|
+
|
|
624
|
+
# Check if app exists
|
|
625
|
+
app_exists: bool = False
|
|
626
|
+
try:
|
|
627
|
+
existing_app: App = self.w.apps.get(name=app_name)
|
|
628
|
+
app_exists = True
|
|
629
|
+
logger.debug("App already exists, updating", app_name=app_name)
|
|
630
|
+
except NotFound:
|
|
631
|
+
logger.debug("Creating new app", app_name=app_name)
|
|
632
|
+
|
|
633
|
+
# Create or get the app
|
|
634
|
+
if not app_exists:
|
|
635
|
+
logger.info("Creating Databricks App", app_name=app_name)
|
|
636
|
+
app_spec = App(
|
|
637
|
+
name=app_name,
|
|
638
|
+
description=config.app.description or f"DAO AI Agent: {app_name}",
|
|
639
|
+
)
|
|
640
|
+
app: App = self.w.apps.create_and_wait(app=app_spec)
|
|
641
|
+
logger.info("App created", app_name=app.name, app_url=app.url)
|
|
642
|
+
else:
|
|
643
|
+
app = existing_app
|
|
644
|
+
|
|
645
|
+
# Deploy the app with source code
|
|
646
|
+
# The app will use the dao_ai.app_server module as the entry point
|
|
647
|
+
logger.info("Deploying app", app_name=app_name)
|
|
648
|
+
|
|
649
|
+
# Create deployment configuration
|
|
650
|
+
app_deployment = AppDeployment(
|
|
651
|
+
mode=AppDeploymentMode.SNAPSHOT,
|
|
652
|
+
source_code_path=source_path,
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
# Deploy the app
|
|
656
|
+
deployment: AppDeployment = self.w.apps.deploy_and_wait(
|
|
657
|
+
app_name=app_name,
|
|
658
|
+
app_deployment=app_deployment,
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
if (
|
|
662
|
+
deployment.status
|
|
663
|
+
and deployment.status.state == AppDeploymentState.SUCCEEDED
|
|
664
|
+
):
|
|
665
|
+
logger.info(
|
|
666
|
+
"App deployed successfully",
|
|
667
|
+
app_name=app_name,
|
|
668
|
+
deployment_id=deployment.deployment_id,
|
|
669
|
+
app_url=app.url if app else None,
|
|
670
|
+
)
|
|
671
|
+
else:
|
|
672
|
+
status_message: str = (
|
|
673
|
+
deployment.status.message if deployment.status else "Unknown error"
|
|
674
|
+
)
|
|
675
|
+
logger.error(
|
|
676
|
+
"App deployment failed",
|
|
677
|
+
app_name=app_name,
|
|
678
|
+
status=status_message,
|
|
679
|
+
)
|
|
680
|
+
raise RuntimeError(f"App deployment failed: {status_message}")
|
|
681
|
+
|
|
682
|
+
def deploy_agent(
|
|
683
|
+
self,
|
|
684
|
+
config: AppConfig,
|
|
685
|
+
target: DeploymentTarget = DeploymentTarget.MODEL_SERVING,
|
|
686
|
+
) -> None:
|
|
687
|
+
"""
|
|
688
|
+
Deploy agent to the specified target.
|
|
689
|
+
|
|
690
|
+
This is the main deployment method that routes to the appropriate
|
|
691
|
+
deployment implementation based on the target.
|
|
692
|
+
|
|
693
|
+
Args:
|
|
694
|
+
config: The AppConfig containing deployment configuration
|
|
695
|
+
target: The deployment target (MODEL_SERVING or APPS)
|
|
696
|
+
"""
|
|
697
|
+
if target == DeploymentTarget.MODEL_SERVING:
|
|
698
|
+
self.deploy_model_serving_agent(config)
|
|
699
|
+
elif target == DeploymentTarget.APPS:
|
|
700
|
+
self.deploy_apps_agent(config)
|
|
701
|
+
else:
|
|
702
|
+
raise ValueError(f"Unknown deployment target: {target}")
|
|
703
|
+
|
|
502
704
|
def create_catalog(self, schema: SchemaModel) -> CatalogInfo:
|
|
503
705
|
catalog_info: CatalogInfo
|
|
504
706
|
try:
|
dao_ai/state.py
CHANGED
dao_ai/tools/mcp.py
CHANGED
|
@@ -26,9 +26,9 @@ from loguru import logger
|
|
|
26
26
|
from mcp.types import CallToolResult, TextContent, Tool
|
|
27
27
|
|
|
28
28
|
from dao_ai.config import (
|
|
29
|
+
IsDatabricksResource,
|
|
29
30
|
McpFunctionModel,
|
|
30
31
|
TransportType,
|
|
31
|
-
value_of,
|
|
32
32
|
)
|
|
33
33
|
|
|
34
34
|
|
|
@@ -143,12 +143,54 @@ def _should_include_tool(
|
|
|
143
143
|
return True
|
|
144
144
|
|
|
145
145
|
|
|
146
|
+
def _get_auth_resource(function: McpFunctionModel) -> IsDatabricksResource:
|
|
147
|
+
"""
|
|
148
|
+
Get the IsDatabricksResource to use for authentication.
|
|
149
|
+
|
|
150
|
+
Follows a priority hierarchy:
|
|
151
|
+
1. Explicit resource with auth (app, connection, genie_room, vector_search, functions)
|
|
152
|
+
2. McpFunctionModel itself (which also inherits from IsDatabricksResource)
|
|
153
|
+
|
|
154
|
+
Returns the resource whose workspace_client should be used for authentication.
|
|
155
|
+
"""
|
|
156
|
+
# Check each possible resource source in priority order
|
|
157
|
+
# These resources may have their own auth configured
|
|
158
|
+
if function.app:
|
|
159
|
+
return function.app
|
|
160
|
+
if function.connection:
|
|
161
|
+
return function.connection
|
|
162
|
+
if function.genie_room:
|
|
163
|
+
return function.genie_room
|
|
164
|
+
if function.vector_search:
|
|
165
|
+
return function.vector_search
|
|
166
|
+
if function.functions:
|
|
167
|
+
# SchemaModel doesn't have auth - fall through to McpFunctionModel
|
|
168
|
+
pass
|
|
169
|
+
|
|
170
|
+
# Fall back to McpFunctionModel itself (it inherits from IsDatabricksResource)
|
|
171
|
+
return function
|
|
172
|
+
|
|
173
|
+
|
|
146
174
|
def _build_connection_config(
|
|
147
175
|
function: McpFunctionModel,
|
|
148
176
|
) -> dict[str, Any]:
|
|
149
177
|
"""
|
|
150
178
|
Build the connection configuration dictionary for MultiServerMCPClient.
|
|
151
179
|
|
|
180
|
+
Authentication Strategy:
|
|
181
|
+
-----------------------
|
|
182
|
+
For HTTP transport, authentication is handled consistently using
|
|
183
|
+
DatabricksOAuthClientProvider with the workspace_client from the appropriate
|
|
184
|
+
IsDatabricksResource. The auth resource is selected in this priority:
|
|
185
|
+
|
|
186
|
+
1. Nested resource (app, connection, genie_room, vector_search) if it has auth
|
|
187
|
+
2. McpFunctionModel itself (inherits from IsDatabricksResource)
|
|
188
|
+
|
|
189
|
+
This approach ensures:
|
|
190
|
+
- Consistent auth handling across all MCP sources
|
|
191
|
+
- Automatic token refresh for long-running connections
|
|
192
|
+
- Support for OBO, service principal, PAT, and ambient auth
|
|
193
|
+
|
|
152
194
|
Args:
|
|
153
195
|
function: The MCP function model configuration.
|
|
154
196
|
|
|
@@ -162,52 +204,30 @@ def _build_connection_config(
|
|
|
162
204
|
"transport": function.transport.value,
|
|
163
205
|
}
|
|
164
206
|
|
|
165
|
-
# For HTTP transport
|
|
166
|
-
|
|
167
|
-
from databricks_mcp import DatabricksOAuthClientProvider
|
|
207
|
+
# For HTTP transport, use DatabricksOAuthClientProvider with unified auth
|
|
208
|
+
from databricks_mcp import DatabricksOAuthClientProvider
|
|
168
209
|
|
|
169
|
-
|
|
170
|
-
|
|
210
|
+
# Get the resource to use for authentication
|
|
211
|
+
auth_resource = _get_auth_resource(function)
|
|
171
212
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
return {
|
|
178
|
-
"url": function.mcp_url,
|
|
179
|
-
"transport": "http",
|
|
180
|
-
"auth": auth_provider,
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
# For HTTP transport with headers-based authentication
|
|
184
|
-
headers: dict[str, str] = {
|
|
185
|
-
key: str(value_of(val)) for key, val in function.headers.items()
|
|
186
|
-
}
|
|
213
|
+
# Get workspace client from the auth resource
|
|
214
|
+
workspace_client = auth_resource.workspace_client
|
|
215
|
+
auth_provider = DatabricksOAuthClientProvider(workspace_client)
|
|
187
216
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
client_secret=value_of(function.client_secret),
|
|
198
|
-
pat=value_of(function.pat),
|
|
199
|
-
)
|
|
200
|
-
headers["Authorization"] = f"Bearer {provider.create_token()}"
|
|
201
|
-
logger.trace("Generated fresh authentication token")
|
|
202
|
-
except Exception as e:
|
|
203
|
-
logger.error("Failed to create fresh token", error=str(e))
|
|
204
|
-
else:
|
|
205
|
-
logger.trace("Using existing authentication token")
|
|
217
|
+
# Log which resource is providing auth
|
|
218
|
+
resource_name = (
|
|
219
|
+
getattr(auth_resource, "name", None) or auth_resource.__class__.__name__
|
|
220
|
+
)
|
|
221
|
+
logger.trace(
|
|
222
|
+
"Using DatabricksOAuthClientProvider for authentication",
|
|
223
|
+
auth_resource=resource_name,
|
|
224
|
+
resource_type=auth_resource.__class__.__name__,
|
|
225
|
+
)
|
|
206
226
|
|
|
207
227
|
return {
|
|
208
228
|
"url": function.mcp_url,
|
|
209
229
|
"transport": "http",
|
|
210
|
-
"
|
|
230
|
+
"auth": auth_provider,
|
|
211
231
|
}
|
|
212
232
|
|
|
213
233
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dao-ai
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.9
|
|
4
4
|
Summary: DAO AI: A modular, multi-agent orchestration framework for complex AI workflows. Supports agent handoff, tool integration, and dynamic configuration via YAML.
|
|
5
5
|
Project-URL: Homepage, https://github.com/natefleming/dao-ai
|
|
6
6
|
Project-URL: Documentation, https://natefleming.github.io/dao-ai
|
|
@@ -43,7 +43,7 @@ Requires-Dist: langgraph>=1.0.5
|
|
|
43
43
|
Requires-Dist: langmem>=0.0.30
|
|
44
44
|
Requires-Dist: loguru>=0.7.3
|
|
45
45
|
Requires-Dist: mcp>=1.24.0
|
|
46
|
-
Requires-Dist: mlflow>=3.8.1
|
|
46
|
+
Requires-Dist: mlflow[databricks]>=3.8.1
|
|
47
47
|
Requires-Dist: nest-asyncio>=1.6.0
|
|
48
48
|
Requires-Dist: openevals>=0.1.3
|
|
49
49
|
Requires-Dist: openpyxl>=3.1.5
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
dao_ai/__init__.py,sha256=18P98ExEgUaJ1Byw440Ct1ty59v6nxyWtc5S6Uq2m9Q,1062
|
|
2
2
|
dao_ai/agent_as_code.py,sha256=xIlLDpPVfmDVzLvbdY_V_CrC4Jvj2ItCWJ-NzdrszTo,538
|
|
3
|
+
dao_ai/app_server.py,sha256=QKpl068z-s1gLF67dPW-3fT77i33t_Oab4_ugmxISWs,3010
|
|
3
4
|
dao_ai/catalog.py,sha256=sPZpHTD3lPx4EZUtIWeQV7VQM89WJ6YH__wluk1v2lE,4947
|
|
4
|
-
dao_ai/cli.py,sha256=
|
|
5
|
-
dao_ai/config.py,sha256=
|
|
5
|
+
dao_ai/cli.py,sha256=57Kmmi0zgS92ACBTD-gH5hZzW6rPDkbdkRVlFjX4onQ,48604
|
|
6
|
+
dao_ai/config.py,sha256=t7kXU7XjdMaCZ3G9Hn-O9NDOaTS_LaMXX6s5mdyv3dM,127944
|
|
6
7
|
dao_ai/graph.py,sha256=1-uQlo7iXZQTT3uU8aYu0N5rnhw5_g_2YLwVsAs6M-U,1119
|
|
7
8
|
dao_ai/logging.py,sha256=lYy4BmucCHvwW7aI3YQkQXKJtMvtTnPDu9Hnd7_O4oc,1556
|
|
8
9
|
dao_ai/messages.py,sha256=4ZBzO4iFdktGSLrmhHzFjzMIt2tpaL-aQLHOQJysGnY,6959
|
|
@@ -10,7 +11,7 @@ dao_ai/models.py,sha256=AwzwTRTNZF-UOh59HsuXEgFk_YH6q6M-mERNDe64Z8k,81783
|
|
|
10
11
|
dao_ai/nodes.py,sha256=7W6Ek6Uk9-pKa-H06nVCwuDllCrgX02IYy3rHtuL0aM,10777
|
|
11
12
|
dao_ai/optimization.py,sha256=phK6t4wYmWPObCjGUBHdZzsaFXGhQOjhAek2bAEfwXo,22971
|
|
12
13
|
dao_ai/prompts.py,sha256=4cz5bZ7cOzrjyQ8hMp-K4evK6cVYrkGrAGdUl8-KDEM,2784
|
|
13
|
-
dao_ai/state.py,sha256=
|
|
14
|
+
dao_ai/state.py,sha256=ifDTAC7epdowk3Z1CP3Xqw4uH2dIxQEVF3C747dA8yI,6436
|
|
14
15
|
dao_ai/types.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
16
|
dao_ai/utils.py,sha256=_Urd7Nj2VzrgPKf3NS4E6vt0lWRhEUddBqWN9BksqeE,11543
|
|
16
17
|
dao_ai/vector_search.py,sha256=8d3xROg9zSIYNXjRRl6rSexsJTlufjRl5Fy1ZA8daKA,4019
|
|
@@ -48,14 +49,14 @@ dao_ai/orchestration/core.py,sha256=qoU7uMXBJCth-sqfu0jRE1L0GOn5H4LoZdRUY1Ib3DI,
|
|
|
48
49
|
dao_ai/orchestration/supervisor.py,sha256=alKMEEo9G5LhdpMvTVdAMel234cZj5_MguWl4wFB7XQ,9873
|
|
49
50
|
dao_ai/orchestration/swarm.py,sha256=8tp1eGmsQqqWpaDcjPoJckddPWohZdmmN0RGRJ_xzOA,9198
|
|
50
51
|
dao_ai/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
51
|
-
dao_ai/providers/base.py,sha256
|
|
52
|
-
dao_ai/providers/databricks.py,sha256=
|
|
52
|
+
dao_ai/providers/base.py,sha256=cJGo3UjUTPgS91dv38ePOHwQQtYhIa84ebb167CBXjk,2111
|
|
53
|
+
dao_ai/providers/databricks.py,sha256=X_VjzZogwiSlNpPBWP0iMCCXAvfFDRlbC4AZCHleb2A,68608
|
|
53
54
|
dao_ai/tools/__init__.py,sha256=NfRpAKds_taHbx6gzLPWgtPXve-YpwzkoOAUflwxceM,1734
|
|
54
55
|
dao_ai/tools/agent.py,sha256=plIWALywRjaDSnot13nYehBsrHRpBUpsVZakoGeajOE,1858
|
|
55
56
|
dao_ai/tools/core.py,sha256=bRIN3BZhRQX8-Kpu3HPomliodyskCqjxynQmYbk6Vjs,3783
|
|
56
57
|
dao_ai/tools/email.py,sha256=A3TsCoQgJR7UUWR0g45OPRGDpVoYwctFs1MOZMTt_d4,7389
|
|
57
58
|
dao_ai/tools/genie.py,sha256=4e_5MeAe7kDzHbYeXuNPFbY5z8ci3ouj8l5254CZ2lA,8874
|
|
58
|
-
dao_ai/tools/mcp.py,sha256=
|
|
59
|
+
dao_ai/tools/mcp.py,sha256=ZNalYo2atZECatZjMT8w4mHEsaUZJQ_fsCjia7px1nc,18689
|
|
59
60
|
dao_ai/tools/memory.py,sha256=lwObKimAand22Nq3Y63tsv-AXQ5SXUigN9PqRjoWKes,1836
|
|
60
61
|
dao_ai/tools/python.py,sha256=jWFnZPni2sCdtd8D1CqXnZIPHnWkdK27bCJnBXpzhvo,1879
|
|
61
62
|
dao_ai/tools/search.py,sha256=cJ3D9FKr1GAR6xz55dLtRkjtQsI0WRueGt9TPDFpOxc,433
|
|
@@ -64,8 +65,8 @@ dao_ai/tools/sql.py,sha256=tKd1gjpLuKdQDyfmyYYtMiNRHDW6MGRbdEVaeqyB8Ok,7632
|
|
|
64
65
|
dao_ai/tools/time.py,sha256=tufJniwivq29y0LIffbgeBTIDE6VgrLpmVf8Qr90qjw,9224
|
|
65
66
|
dao_ai/tools/unity_catalog.py,sha256=AjQfW7bvV8NurqDLIyntYRv2eJuTwNdbvex1L5CRjOk,15534
|
|
66
67
|
dao_ai/tools/vector_search.py,sha256=oe2uBwl2TfeJIXPpwiS6Rmz7wcHczSxNyqS9P3hE6co,14542
|
|
67
|
-
dao_ai-0.1.
|
|
68
|
-
dao_ai-0.1.
|
|
69
|
-
dao_ai-0.1.
|
|
70
|
-
dao_ai-0.1.
|
|
71
|
-
dao_ai-0.1.
|
|
68
|
+
dao_ai-0.1.9.dist-info/METADATA,sha256=_cVhNKdywyci5ZyrmMfkF9CiWveo4kdoj9OlaM_w-cg,16697
|
|
69
|
+
dao_ai-0.1.9.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
70
|
+
dao_ai-0.1.9.dist-info/entry_points.txt,sha256=Xa-UFyc6gWGwMqMJOt06ZOog2vAfygV_DSwg1AiP46g,43
|
|
71
|
+
dao_ai-0.1.9.dist-info/licenses/LICENSE,sha256=YZt3W32LtPYruuvHE9lGk2bw6ZPMMJD8yLrjgHybyz4,1069
|
|
72
|
+
dao_ai-0.1.9.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|