dao-ai 0.1.9__py3-none-any.whl → 0.1.10__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/apps/server.py ADDED
@@ -0,0 +1,39 @@
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 model_serving.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.apps.server
15
+
16
+ # With default model_config.yaml in current directory
17
+ python -m dao_ai.apps.server
18
+ """
19
+
20
+ from mlflow.genai.agent_server import AgentServer
21
+
22
+ # Import the agent handlers to register the invoke and stream decorators
23
+ # This MUST happen before creating the AgentServer instance
24
+ import dao_ai.apps.handlers # noqa: E402, F401
25
+
26
+ # Create the AgentServer instance
27
+ agent_server = AgentServer("ResponsesAgent", enable_chat_proxy=True)
28
+
29
+ # Define the app as a module level variable to enable multiple workers
30
+ app = agent_server.app
31
+
32
+
33
+ def main() -> None:
34
+ """Entry point for running the agent server."""
35
+ agent_server.run(app_import_string="dao_ai.apps.server:app")
36
+
37
+
38
+ if __name__ == "__main__":
39
+ main()
dao_ai/cli.py CHANGED
@@ -285,6 +285,15 @@ Examples:
285
285
  action="store_true",
286
286
  help="Perform a dry run without executing the deployment or run commands",
287
287
  )
288
+ bundle_parser.add_argument(
289
+ "--deployment-target",
290
+ type=str,
291
+ choices=["model_serving", "apps"],
292
+ default=None,
293
+ help="Agent deployment target: 'model_serving' or 'apps'. "
294
+ "If not specified, uses app.deployment_target from config file, "
295
+ "or defaults to 'model_serving'. Passed to the deploy notebook.",
296
+ )
288
297
 
289
298
  # Deploy command
290
299
  deploy_parser: ArgumentParser = subparsers.add_parser(
@@ -314,8 +323,10 @@ Examples:
314
323
  "--target",
315
324
  type=str,
316
325
  choices=["model_serving", "apps"],
317
- default="model_serving",
318
- help="Deployment target: 'model_serving' (default) or 'apps'",
326
+ default=None,
327
+ help="Deployment target: 'model_serving' or 'apps'. "
328
+ "If not specified, uses app.deployment_target from config file, "
329
+ "or defaults to 'model_serving'.",
319
330
  )
320
331
 
321
332
  # List MCP tools command
@@ -743,8 +754,19 @@ def handle_deploy_command(options: Namespace) -> None:
743
754
  try:
744
755
  config: AppConfig = AppConfig.from_file(options.config)
745
756
 
746
- # Convert target string to enum
747
- target: DeploymentTarget = DeploymentTarget(options.target)
757
+ # Hybrid target resolution:
758
+ # 1. CLI --target takes precedence
759
+ # 2. Fall back to config.app.deployment_target
760
+ # 3. Default to MODEL_SERVING (handled in deploy_agent)
761
+ target: DeploymentTarget | None = None
762
+ if options.target is not None:
763
+ target = DeploymentTarget(options.target)
764
+ logger.info(f"Using CLI-specified deployment target: {target.value}")
765
+ elif config.app is not None and config.app.deployment_target is not None:
766
+ target = config.app.deployment_target
767
+ logger.info(f"Using config file deployment target: {target.value}")
768
+ else:
769
+ logger.info("No deployment target specified, defaulting to model_serving")
748
770
 
749
771
  config.create_agent()
750
772
  config.deploy_agent(target=target)
@@ -1097,6 +1119,7 @@ def run_databricks_command(
1097
1119
  target: Optional[str] = None,
1098
1120
  cloud: Optional[str] = None,
1099
1121
  dry_run: bool = False,
1122
+ deployment_target: Optional[str] = None,
1100
1123
  ) -> None:
1101
1124
  """Execute a databricks CLI command with optional profile, target, and cloud.
1102
1125
 
@@ -1107,6 +1130,8 @@ def run_databricks_command(
1107
1130
  target: Optional bundle target name (if not provided, auto-generated from app name and cloud)
1108
1131
  cloud: Optional cloud provider ('azure', 'aws', 'gcp'). Auto-detected if not specified.
1109
1132
  dry_run: If True, print the command without executing
1133
+ deployment_target: Optional agent deployment target ('model_serving' or 'apps').
1134
+ Passed to the deploy notebook via bundle variable.
1110
1135
  """
1111
1136
  config_path = Path(config) if config else None
1112
1137
 
@@ -1162,6 +1187,24 @@ def run_databricks_command(
1162
1187
 
1163
1188
  cmd.append(f'--var="config_path={relative_config}"')
1164
1189
 
1190
+ # Add deployment_target variable for notebooks (hybrid resolution)
1191
+ # Priority: CLI arg > config file > default (model_serving)
1192
+ resolved_deployment_target: str = "model_serving"
1193
+ if deployment_target is not None:
1194
+ resolved_deployment_target = deployment_target
1195
+ logger.debug(
1196
+ f"Using CLI-specified deployment target: {resolved_deployment_target}"
1197
+ )
1198
+ elif app_config and app_config.app and app_config.app.deployment_target:
1199
+ resolved_deployment_target = app_config.app.deployment_target.value
1200
+ logger.debug(
1201
+ f"Using config file deployment target: {resolved_deployment_target}"
1202
+ )
1203
+ else:
1204
+ logger.debug("Using default deployment target: model_serving")
1205
+
1206
+ cmd.append(f'--var="deployment_target={resolved_deployment_target}"')
1207
+
1165
1208
  logger.debug(f"Executing command: {' '.join(cmd)}")
1166
1209
 
1167
1210
  if dry_run:
@@ -1204,6 +1247,7 @@ def handle_bundle_command(options: Namespace) -> None:
1204
1247
  target: Optional[str] = options.target
1205
1248
  cloud: Optional[str] = options.cloud
1206
1249
  dry_run: bool = options.dry_run
1250
+ deployment_target: Optional[str] = options.deployment_target
1207
1251
 
1208
1252
  if options.deploy:
1209
1253
  logger.info("Deploying DAO AI asset bundle...")
@@ -1214,6 +1258,7 @@ def handle_bundle_command(options: Namespace) -> None:
1214
1258
  target=target,
1215
1259
  cloud=cloud,
1216
1260
  dry_run=dry_run,
1261
+ deployment_target=deployment_target,
1217
1262
  )
1218
1263
  if options.run:
1219
1264
  logger.info("Running DAO AI system with current configuration...")
@@ -1225,6 +1270,7 @@ def handle_bundle_command(options: Namespace) -> None:
1225
1270
  target=target,
1226
1271
  cloud=cloud,
1227
1272
  dry_run=dry_run,
1273
+ deployment_target=deployment_target,
1228
1274
  )
1229
1275
  if options.destroy:
1230
1276
  logger.info("Destroying DAO AI system with current configuration...")
@@ -1235,6 +1281,7 @@ def handle_bundle_command(options: Namespace) -> None:
1235
1281
  target=target,
1236
1282
  cloud=cloud,
1237
1283
  dry_run=dry_run,
1284
+ deployment_target=deployment_target,
1238
1285
  )
1239
1286
  else:
1240
1287
  logger.warning("No action specified. Use --deploy, --run or --destroy flags.")
dao_ai/config.py CHANGED
@@ -344,7 +344,14 @@ class IsDatabricksResource(ABC, BaseModel):
344
344
  else None
345
345
  )
346
346
 
347
- if client_id_value and client_secret_value and workspace_host_value:
347
+ if client_id_value and client_secret_value:
348
+ # If workspace_host is not provided, check DATABRICKS_HOST env var first,
349
+ # then fall back to WorkspaceClient().config.host
350
+ if not workspace_host_value:
351
+ workspace_host_value = os.getenv("DATABRICKS_HOST")
352
+ if not workspace_host_value:
353
+ workspace_host_value = WorkspaceClient().config.host
354
+
348
355
  logger.debug(
349
356
  f"Creating WorkspaceClient for {self.__class__.__name__} with service principal: "
350
357
  f"client_id={client_id_value}, host={workspace_host_value}"
@@ -2803,6 +2810,11 @@ class AppModel(BaseModel):
2803
2810
  "which is supported by Databricks Model Serving. This allows deploying from "
2804
2811
  "environments with different Python versions (e.g., Databricks Apps with 3.11).",
2805
2812
  )
2813
+ deployment_target: Optional[DeploymentTarget] = Field(
2814
+ default=None,
2815
+ description="Default deployment target. If not specified, defaults to MODEL_SERVING. "
2816
+ "Can be overridden via CLI --target flag. Options: 'model_serving' or 'apps'.",
2817
+ )
2806
2818
 
2807
2819
  @model_validator(mode="after")
2808
2820
  def set_databricks_env_vars(self) -> Self:
@@ -3398,7 +3410,7 @@ class AppConfig(BaseModel):
3398
3410
 
3399
3411
  def deploy_agent(
3400
3412
  self,
3401
- target: DeploymentTarget = DeploymentTarget.MODEL_SERVING,
3413
+ target: DeploymentTarget | None = None,
3402
3414
  w: WorkspaceClient | None = None,
3403
3415
  vsc: "VectorSearchClient | None" = None,
3404
3416
  pat: str | None = None,
@@ -3409,8 +3421,14 @@ class AppConfig(BaseModel):
3409
3421
  """
3410
3422
  Deploy the agent to the specified target.
3411
3423
 
3424
+ Target resolution follows this priority:
3425
+ 1. Explicit `target` parameter (if provided)
3426
+ 2. `app.deployment_target` from config file (if set)
3427
+ 3. Default: MODEL_SERVING
3428
+
3412
3429
  Args:
3413
- target: The deployment target (MODEL_SERVING or APPS). Defaults to MODEL_SERVING.
3430
+ target: The deployment target (MODEL_SERVING or APPS). If None, uses
3431
+ config.app.deployment_target or defaults to MODEL_SERVING.
3414
3432
  w: Optional WorkspaceClient instance
3415
3433
  vsc: Optional VectorSearchClient instance
3416
3434
  pat: Optional personal access token for authentication
@@ -3421,6 +3439,18 @@ class AppConfig(BaseModel):
3421
3439
  from dao_ai.providers.base import ServiceProvider
3422
3440
  from dao_ai.providers.databricks import DatabricksProvider
3423
3441
 
3442
+ # Resolve target using hybrid logic:
3443
+ # 1. Explicit parameter takes precedence
3444
+ # 2. Fall back to config.app.deployment_target
3445
+ # 3. Default to MODEL_SERVING
3446
+ resolved_target: DeploymentTarget
3447
+ if target is not None:
3448
+ resolved_target = target
3449
+ elif self.app is not None and self.app.deployment_target is not None:
3450
+ resolved_target = self.app.deployment_target
3451
+ else:
3452
+ resolved_target = DeploymentTarget.MODEL_SERVING
3453
+
3424
3454
  provider: ServiceProvider = DatabricksProvider(
3425
3455
  w=w,
3426
3456
  vsc=vsc,
@@ -3429,7 +3459,7 @@ class AppConfig(BaseModel):
3429
3459
  client_secret=client_secret,
3430
3460
  workspace_host=workspace_host,
3431
3461
  )
3432
- provider.deploy_agent(self, target=target)
3462
+ provider.deploy_agent(self, target=resolved_target)
3433
3463
 
3434
3464
  def find_agents(
3435
3465
  self, predicate: Callable[[AgentModel], bool] | None = None
dao_ai/memory/postgres.py CHANGED
@@ -178,7 +178,20 @@ class AsyncPostgresStoreManager(StoreManagerBase):
178
178
  def _setup(self):
179
179
  if self._setup_complete:
180
180
  return
181
- asyncio.run(self._async_setup())
181
+ try:
182
+ # Check if we're already in an async context
183
+ asyncio.get_running_loop()
184
+ # If we get here, we're in an async context - raise to caller
185
+ raise RuntimeError(
186
+ "Cannot call sync _setup() from async context. "
187
+ "Use await _async_setup() instead."
188
+ )
189
+ except RuntimeError as e:
190
+ if "no running event loop" in str(e).lower():
191
+ # No event loop running - safe to use asyncio.run()
192
+ asyncio.run(self._async_setup())
193
+ else:
194
+ raise
182
195
 
183
196
  async def _async_setup(self):
184
197
  if self._setup_complete:
@@ -237,13 +250,25 @@ class AsyncPostgresCheckpointerManager(CheckpointManagerBase):
237
250
 
238
251
  def _setup(self):
239
252
  """
240
- Run the async setup. Works in both sync and async contexts when nest_asyncio is applied.
253
+ Run the async setup. For async contexts, use await _async_setup() directly.
241
254
  """
242
255
  if self._setup_complete:
243
256
  return
244
257
 
245
- # With nest_asyncio applied in notebooks, asyncio.run() works everywhere
246
- asyncio.run(self._async_setup())
258
+ try:
259
+ # Check if we're already in an async context
260
+ asyncio.get_running_loop()
261
+ # If we get here, we're in an async context - raise to caller
262
+ raise RuntimeError(
263
+ "Cannot call sync _setup() from async context. "
264
+ "Use await _async_setup() instead."
265
+ )
266
+ except RuntimeError as e:
267
+ if "no running event loop" in str(e).lower():
268
+ # No event loop running - safe to use asyncio.run()
269
+ asyncio.run(self._async_setup())
270
+ else:
271
+ raise
247
272
 
248
273
  async def _async_setup(self):
249
274
  """