dao-ai 0.1.5__py3-none-any.whl → 0.1.7__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/cli.py +320 -1
- dao_ai/config.py +119 -19
- dao_ai/middleware/__init__.py +5 -0
- dao_ai/middleware/tool_selector.py +129 -0
- dao_ai/prompts.py +2 -60
- dao_ai/tools/__init__.py +3 -1
- dao_ai/tools/mcp.py +338 -47
- {dao_ai-0.1.5.dist-info → dao_ai-0.1.7.dist-info}/METADATA +1 -1
- {dao_ai-0.1.5.dist-info → dao_ai-0.1.7.dist-info}/RECORD +12 -11
- {dao_ai-0.1.5.dist-info → dao_ai-0.1.7.dist-info}/WHEEL +0 -0
- {dao_ai-0.1.5.dist-info → dao_ai-0.1.7.dist-info}/entry_points.txt +0 -0
- {dao_ai-0.1.5.dist-info → dao_ai-0.1.7.dist-info}/licenses/LICENSE +0 -0
dao_ai/cli.py
CHANGED
|
@@ -7,7 +7,7 @@ import sys
|
|
|
7
7
|
import traceback
|
|
8
8
|
from argparse import ArgumentParser, Namespace
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import Optional, Sequence
|
|
10
|
+
from typing import Any, Optional, Sequence
|
|
11
11
|
|
|
12
12
|
from dotenv import find_dotenv, load_dotenv
|
|
13
13
|
from loguru import logger
|
|
@@ -114,6 +114,7 @@ Examples:
|
|
|
114
114
|
dao-ai validate -c config/model_config.yaml # Validate a specific configuration file
|
|
115
115
|
dao-ai graph -o architecture.png -c my_config.yaml -v # Generate visual graph with verbose output
|
|
116
116
|
dao-ai chat -c config/retail.yaml --custom-input store_num=87887 # Start interactive chat session
|
|
117
|
+
dao-ai list-mcp-tools -c config/mcp_config.yaml --apply-filters # List filtered MCP tools only
|
|
117
118
|
dao-ai validate # Validate with detailed logging
|
|
118
119
|
dao-ai bundle --deploy # Deploy the DAO AI asset bundle
|
|
119
120
|
""",
|
|
@@ -309,6 +310,53 @@ Examples:
|
|
|
309
310
|
help="Path to the model configuration file to validate",
|
|
310
311
|
)
|
|
311
312
|
|
|
313
|
+
# List MCP tools command
|
|
314
|
+
list_mcp_parser: ArgumentParser = subparsers.add_parser(
|
|
315
|
+
"list-mcp-tools",
|
|
316
|
+
help="List available MCP tools from configuration",
|
|
317
|
+
description="""
|
|
318
|
+
List all available MCP tools from the configured MCP servers.
|
|
319
|
+
This command shows:
|
|
320
|
+
- All MCP servers/functions in the configuration
|
|
321
|
+
- Available tools from each server
|
|
322
|
+
- Full descriptions for each tool (no truncation)
|
|
323
|
+
- Tool parameters in readable format (type, required/optional, descriptions)
|
|
324
|
+
- Which tools are included/excluded based on filters
|
|
325
|
+
- Filter patterns (include_tools, exclude_tools)
|
|
326
|
+
|
|
327
|
+
Use this command to:
|
|
328
|
+
- Discover available tools before configuring agents
|
|
329
|
+
- Review tool descriptions and parameter schemas
|
|
330
|
+
- Debug tool filtering configuration
|
|
331
|
+
- Verify MCP server connectivity
|
|
332
|
+
|
|
333
|
+
Options:
|
|
334
|
+
- Use --apply-filters to only show tools that will be loaded (hides excluded tools)
|
|
335
|
+
- Without --apply-filters, see all available tools with include/exclude status
|
|
336
|
+
|
|
337
|
+
Note: Schemas are displayed in a concise, readable format instead of verbose JSON
|
|
338
|
+
""",
|
|
339
|
+
epilog="""Examples:
|
|
340
|
+
dao-ai list-mcp-tools -c config/model_config.yaml
|
|
341
|
+
dao-ai list-mcp-tools -c config/model_config.yaml --apply-filters
|
|
342
|
+
""",
|
|
343
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
344
|
+
)
|
|
345
|
+
list_mcp_parser.add_argument(
|
|
346
|
+
"-c",
|
|
347
|
+
"--config",
|
|
348
|
+
type=str,
|
|
349
|
+
default="./config/model_config.yaml",
|
|
350
|
+
required=False,
|
|
351
|
+
metavar="FILE",
|
|
352
|
+
help="Path to the model configuration file (default: ./config/model_config.yaml)",
|
|
353
|
+
)
|
|
354
|
+
list_mcp_parser.add_argument(
|
|
355
|
+
"--apply-filters",
|
|
356
|
+
action="store_true",
|
|
357
|
+
help="Only show tools that pass include/exclude filters (hide excluded tools)",
|
|
358
|
+
)
|
|
359
|
+
|
|
312
360
|
chat_parser: ArgumentParser = subparsers.add_parser(
|
|
313
361
|
"chat",
|
|
314
362
|
help="Interactive chat with the DAO AI system",
|
|
@@ -704,6 +752,275 @@ def handle_validate_command(options: Namespace) -> None:
|
|
|
704
752
|
sys.exit(1)
|
|
705
753
|
|
|
706
754
|
|
|
755
|
+
def _format_schema_pretty(schema: dict[str, Any], indent: int = 0) -> str:
|
|
756
|
+
"""
|
|
757
|
+
Format a JSON schema in a more readable, concise format.
|
|
758
|
+
|
|
759
|
+
Args:
|
|
760
|
+
schema: The JSON schema to format
|
|
761
|
+
indent: Current indentation level
|
|
762
|
+
|
|
763
|
+
Returns:
|
|
764
|
+
Pretty-formatted schema string
|
|
765
|
+
"""
|
|
766
|
+
if not schema:
|
|
767
|
+
return ""
|
|
768
|
+
|
|
769
|
+
lines: list[str] = []
|
|
770
|
+
indent_str = " " * indent
|
|
771
|
+
|
|
772
|
+
# Get required fields
|
|
773
|
+
required_fields = set(schema.get("required", []))
|
|
774
|
+
|
|
775
|
+
# Handle object type with properties
|
|
776
|
+
if schema.get("type") == "object" and "properties" in schema:
|
|
777
|
+
properties = schema["properties"]
|
|
778
|
+
|
|
779
|
+
for prop_name, prop_schema in properties.items():
|
|
780
|
+
is_required = prop_name in required_fields
|
|
781
|
+
req_marker = " (required)" if is_required else " (optional)"
|
|
782
|
+
|
|
783
|
+
prop_type = prop_schema.get("type", "any")
|
|
784
|
+
prop_desc = prop_schema.get("description", "")
|
|
785
|
+
|
|
786
|
+
# Handle different types
|
|
787
|
+
if prop_type == "array":
|
|
788
|
+
items = prop_schema.get("items", {})
|
|
789
|
+
item_type = items.get("type", "any")
|
|
790
|
+
type_str = f"array<{item_type}>"
|
|
791
|
+
elif prop_type == "object":
|
|
792
|
+
type_str = "object"
|
|
793
|
+
else:
|
|
794
|
+
type_str = prop_type
|
|
795
|
+
|
|
796
|
+
# Format enum values if present
|
|
797
|
+
if "enum" in prop_schema:
|
|
798
|
+
enum_values = ", ".join(str(v) for v in prop_schema["enum"])
|
|
799
|
+
type_str = f"{type_str} (one of: {enum_values})"
|
|
800
|
+
|
|
801
|
+
# Build the line
|
|
802
|
+
line = f"{indent_str}{prop_name}: {type_str}{req_marker}"
|
|
803
|
+
if prop_desc:
|
|
804
|
+
line += f"\n{indent_str} └─ {prop_desc}"
|
|
805
|
+
|
|
806
|
+
lines.append(line)
|
|
807
|
+
|
|
808
|
+
# Recursively handle nested objects
|
|
809
|
+
if prop_type == "object" and "properties" in prop_schema:
|
|
810
|
+
nested = _format_schema_pretty(prop_schema, indent + 1)
|
|
811
|
+
if nested:
|
|
812
|
+
lines.append(nested)
|
|
813
|
+
|
|
814
|
+
# Handle simple types without properties
|
|
815
|
+
elif "type" in schema:
|
|
816
|
+
schema_type = schema["type"]
|
|
817
|
+
if schema.get("description"):
|
|
818
|
+
lines.append(f"{indent_str}Type: {schema_type}")
|
|
819
|
+
lines.append(f"{indent_str}└─ {schema['description']}")
|
|
820
|
+
else:
|
|
821
|
+
lines.append(f"{indent_str}Type: {schema_type}")
|
|
822
|
+
|
|
823
|
+
return "\n".join(lines)
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
def handle_list_mcp_tools_command(options: Namespace) -> None:
|
|
827
|
+
"""
|
|
828
|
+
List available MCP tools from configuration.
|
|
829
|
+
|
|
830
|
+
Shows all MCP servers and their available tools, indicating which
|
|
831
|
+
are included/excluded based on filter configuration.
|
|
832
|
+
"""
|
|
833
|
+
logger.debug(f"Listing MCP tools from configuration: {options.config}")
|
|
834
|
+
|
|
835
|
+
try:
|
|
836
|
+
from dao_ai.config import McpFunctionModel
|
|
837
|
+
from dao_ai.tools.mcp import MCPToolInfo, _matches_pattern, list_mcp_tools
|
|
838
|
+
|
|
839
|
+
# Load configuration
|
|
840
|
+
config: AppConfig = AppConfig.from_file(options.config)
|
|
841
|
+
|
|
842
|
+
# Find all MCP tools in configuration
|
|
843
|
+
mcp_tools_config: list[tuple[str, McpFunctionModel]] = []
|
|
844
|
+
if config.tools:
|
|
845
|
+
for tool_name, tool_model in config.tools.items():
|
|
846
|
+
logger.debug(
|
|
847
|
+
f"Checking tool: {tool_name}, function type: {type(tool_model.function)}"
|
|
848
|
+
)
|
|
849
|
+
if tool_model.function and isinstance(
|
|
850
|
+
tool_model.function, McpFunctionModel
|
|
851
|
+
):
|
|
852
|
+
mcp_tools_config.append((tool_name, tool_model.function))
|
|
853
|
+
|
|
854
|
+
if not mcp_tools_config:
|
|
855
|
+
logger.warning("No MCP tools found in configuration")
|
|
856
|
+
print("\n⚠️ No MCP tools configured in this file.")
|
|
857
|
+
print(f" Configuration: {options.config}")
|
|
858
|
+
print(
|
|
859
|
+
"\nTo add MCP tools, define them in the 'tools' section with 'type: mcp'"
|
|
860
|
+
)
|
|
861
|
+
sys.exit(0)
|
|
862
|
+
|
|
863
|
+
# Collect all results first (aggregate before displaying)
|
|
864
|
+
results: list[dict[str, Any]] = []
|
|
865
|
+
for tool_name, mcp_function in mcp_tools_config:
|
|
866
|
+
result = {
|
|
867
|
+
"tool_name": tool_name,
|
|
868
|
+
"mcp_function": mcp_function,
|
|
869
|
+
"error": None,
|
|
870
|
+
"all_tools": [],
|
|
871
|
+
"included_tools": [],
|
|
872
|
+
"excluded_tools": [],
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
try:
|
|
876
|
+
logger.info(f"Connecting to MCP server: {mcp_function.mcp_url}")
|
|
877
|
+
|
|
878
|
+
# Get all available tools (unfiltered)
|
|
879
|
+
all_tools: list[MCPToolInfo] = list_mcp_tools(
|
|
880
|
+
mcp_function, apply_filters=False
|
|
881
|
+
)
|
|
882
|
+
|
|
883
|
+
# Get filtered tools (what will actually be loaded)
|
|
884
|
+
filtered_tools: list[MCPToolInfo] = list_mcp_tools(
|
|
885
|
+
mcp_function, apply_filters=True
|
|
886
|
+
)
|
|
887
|
+
|
|
888
|
+
included_names = {t.name for t in filtered_tools}
|
|
889
|
+
|
|
890
|
+
# Categorize tools
|
|
891
|
+
for tool in sorted(all_tools, key=lambda t: t.name):
|
|
892
|
+
if tool.name in included_names:
|
|
893
|
+
result["included_tools"].append(tool)
|
|
894
|
+
else:
|
|
895
|
+
# Determine why it was excluded
|
|
896
|
+
reason = ""
|
|
897
|
+
if mcp_function.exclude_tools:
|
|
898
|
+
if _matches_pattern(tool.name, mcp_function.exclude_tools):
|
|
899
|
+
matching_patterns = [
|
|
900
|
+
p
|
|
901
|
+
for p in mcp_function.exclude_tools
|
|
902
|
+
if _matches_pattern(tool.name, [p])
|
|
903
|
+
]
|
|
904
|
+
reason = f" (matches exclude pattern: {', '.join(matching_patterns)})"
|
|
905
|
+
if not reason and mcp_function.include_tools:
|
|
906
|
+
reason = " (not in include list)"
|
|
907
|
+
result["excluded_tools"].append((tool, reason))
|
|
908
|
+
|
|
909
|
+
result["all_tools"] = all_tools
|
|
910
|
+
|
|
911
|
+
except KeyboardInterrupt:
|
|
912
|
+
result["error"] = "Connection interrupted by user"
|
|
913
|
+
results.append(result)
|
|
914
|
+
break
|
|
915
|
+
except Exception as e:
|
|
916
|
+
logger.error(f"Failed to list tools from MCP server: {e}")
|
|
917
|
+
result["error"] = str(e)
|
|
918
|
+
|
|
919
|
+
results.append(result)
|
|
920
|
+
|
|
921
|
+
# Now display all results at once (no logging interleaving)
|
|
922
|
+
print(f"\n{'=' * 80}")
|
|
923
|
+
print("MCP TOOLS DISCOVERY")
|
|
924
|
+
print(f"Configuration: {options.config}")
|
|
925
|
+
print(f"{'=' * 80}\n")
|
|
926
|
+
|
|
927
|
+
for result in results:
|
|
928
|
+
tool_name = result["tool_name"]
|
|
929
|
+
mcp_function = result["mcp_function"]
|
|
930
|
+
|
|
931
|
+
print(f"📦 Tool: {tool_name}")
|
|
932
|
+
print(f" Server: {mcp_function.mcp_url}")
|
|
933
|
+
|
|
934
|
+
# Show connection type
|
|
935
|
+
if mcp_function.connection:
|
|
936
|
+
print(f" Connection: UC Connection '{mcp_function.connection.name}'")
|
|
937
|
+
else:
|
|
938
|
+
print(f" Transport: {mcp_function.transport.value}")
|
|
939
|
+
|
|
940
|
+
# Show filters if configured
|
|
941
|
+
if mcp_function.include_tools or mcp_function.exclude_tools:
|
|
942
|
+
print("\n Filters:")
|
|
943
|
+
if mcp_function.include_tools:
|
|
944
|
+
print(f" Include: {', '.join(mcp_function.include_tools)}")
|
|
945
|
+
if mcp_function.exclude_tools:
|
|
946
|
+
print(f" Exclude: {', '.join(mcp_function.exclude_tools)}")
|
|
947
|
+
|
|
948
|
+
# Check for errors
|
|
949
|
+
if result["error"]:
|
|
950
|
+
print(f"\n ❌ Error: {result['error']}")
|
|
951
|
+
print(" Could not connect to MCP server")
|
|
952
|
+
if result["error"] != "Connection interrupted by user":
|
|
953
|
+
print(
|
|
954
|
+
" Tip: Verify server URL, authentication, and network connectivity"
|
|
955
|
+
)
|
|
956
|
+
else:
|
|
957
|
+
all_tools = result["all_tools"]
|
|
958
|
+
included_tools = result["included_tools"]
|
|
959
|
+
excluded_tools = result["excluded_tools"]
|
|
960
|
+
|
|
961
|
+
# Show stats based on --apply-filters flag
|
|
962
|
+
if options.apply_filters:
|
|
963
|
+
# Simplified view: only show filtered tools count
|
|
964
|
+
print(
|
|
965
|
+
f"\n Available Tools: {len(included_tools)} (after filters)"
|
|
966
|
+
)
|
|
967
|
+
else:
|
|
968
|
+
# Full view: show all, included, and excluded counts
|
|
969
|
+
print(f"\n Available Tools: {len(all_tools)} total")
|
|
970
|
+
print(f" ├─ ✓ Included: {len(included_tools)}")
|
|
971
|
+
print(f" └─ ✗ Excluded: {len(excluded_tools)}")
|
|
972
|
+
|
|
973
|
+
# Show included tools with FULL descriptions and schemas
|
|
974
|
+
if included_tools:
|
|
975
|
+
if options.apply_filters:
|
|
976
|
+
print(f"\n Tools ({len(included_tools)}):")
|
|
977
|
+
else:
|
|
978
|
+
print(f"\n ✓ Included Tools ({len(included_tools)}):")
|
|
979
|
+
|
|
980
|
+
for tool in included_tools:
|
|
981
|
+
print(f"\n • {tool.name}")
|
|
982
|
+
if tool.description:
|
|
983
|
+
# Show full description (no truncation)
|
|
984
|
+
print(f" Description: {tool.description}")
|
|
985
|
+
if tool.input_schema:
|
|
986
|
+
# Pretty print schema in readable format
|
|
987
|
+
print(" Parameters:")
|
|
988
|
+
pretty_schema = _format_schema_pretty(
|
|
989
|
+
tool.input_schema, indent=0
|
|
990
|
+
)
|
|
991
|
+
if pretty_schema:
|
|
992
|
+
# Indent the schema for better readability
|
|
993
|
+
for line in pretty_schema.split("\n"):
|
|
994
|
+
print(f" {line}")
|
|
995
|
+
else:
|
|
996
|
+
print(" (none)")
|
|
997
|
+
|
|
998
|
+
# Show excluded tools only if NOT applying filters
|
|
999
|
+
if excluded_tools and not options.apply_filters:
|
|
1000
|
+
print(f"\n ✗ Excluded Tools ({len(excluded_tools)}):")
|
|
1001
|
+
for tool, reason in excluded_tools:
|
|
1002
|
+
print(f" • {tool.name}{reason}")
|
|
1003
|
+
|
|
1004
|
+
print(f"\n{'-' * 80}\n")
|
|
1005
|
+
|
|
1006
|
+
# Summary
|
|
1007
|
+
print(f"{'=' * 80}")
|
|
1008
|
+
print(f"Summary: Found {len(mcp_tools_config)} MCP server(s)")
|
|
1009
|
+
print(f"{'=' * 80}\n")
|
|
1010
|
+
|
|
1011
|
+
sys.exit(0)
|
|
1012
|
+
|
|
1013
|
+
except FileNotFoundError:
|
|
1014
|
+
logger.error(f"Configuration file not found: {options.config}")
|
|
1015
|
+
print(f"\n❌ Error: Configuration file not found: {options.config}")
|
|
1016
|
+
sys.exit(1)
|
|
1017
|
+
except Exception as e:
|
|
1018
|
+
logger.error(f"Failed to list MCP tools: {e}")
|
|
1019
|
+
logger.debug(traceback.format_exc())
|
|
1020
|
+
print(f"\n❌ Error: {e}")
|
|
1021
|
+
sys.exit(1)
|
|
1022
|
+
|
|
1023
|
+
|
|
707
1024
|
def setup_logging(verbosity: int) -> None:
|
|
708
1025
|
levels: dict[int, str] = {
|
|
709
1026
|
0: "ERROR",
|
|
@@ -925,6 +1242,8 @@ def main() -> None:
|
|
|
925
1242
|
handle_deploy_command(options)
|
|
926
1243
|
case "chat":
|
|
927
1244
|
handle_chat_command(options)
|
|
1245
|
+
case "list-mcp-tools":
|
|
1246
|
+
handle_list_mcp_tools_command(options)
|
|
928
1247
|
case _:
|
|
929
1248
|
logger.error(f"Unknown command: {options.command}")
|
|
930
1249
|
sys.exit(1)
|
dao_ai/config.py
CHANGED
|
@@ -24,6 +24,7 @@ from databricks.sdk.credentials_provider import (
|
|
|
24
24
|
ModelServingUserCredentials,
|
|
25
25
|
)
|
|
26
26
|
from databricks.sdk.errors.platform import NotFound
|
|
27
|
+
from databricks.sdk.service.apps import App
|
|
27
28
|
from databricks.sdk.service.catalog import FunctionInfo, TableInfo
|
|
28
29
|
from databricks.sdk.service.dashboards import GenieSpace
|
|
29
30
|
from databricks.sdk.service.database import DatabaseInstance
|
|
@@ -147,7 +148,7 @@ class PrimitiveVariableModel(BaseModel, HasValue):
|
|
|
147
148
|
return str(value)
|
|
148
149
|
|
|
149
150
|
@model_validator(mode="after")
|
|
150
|
-
def validate_value(self) ->
|
|
151
|
+
def validate_value(self) -> Self:
|
|
151
152
|
if not isinstance(self.as_value(), (str, int, float, bool)):
|
|
152
153
|
raise ValueError("Value must be a primitive type (str, int, float, bool)")
|
|
153
154
|
return self
|
|
@@ -391,10 +392,17 @@ class PermissionModel(BaseModel):
|
|
|
391
392
|
|
|
392
393
|
class SchemaModel(BaseModel, HasFullName):
|
|
393
394
|
model_config = ConfigDict(use_enum_values=True, extra="forbid")
|
|
394
|
-
catalog_name:
|
|
395
|
-
schema_name:
|
|
395
|
+
catalog_name: AnyVariable
|
|
396
|
+
schema_name: AnyVariable
|
|
396
397
|
permissions: Optional[list[PermissionModel]] = Field(default_factory=list)
|
|
397
398
|
|
|
399
|
+
@model_validator(mode="after")
|
|
400
|
+
def resolve_variables(self) -> Self:
|
|
401
|
+
"""Resolve AnyVariable fields to their actual string values."""
|
|
402
|
+
self.catalog_name = value_of(self.catalog_name)
|
|
403
|
+
self.schema_name = value_of(self.schema_name)
|
|
404
|
+
return self
|
|
405
|
+
|
|
398
406
|
@property
|
|
399
407
|
def full_name(self) -> str:
|
|
400
408
|
return f"{self.catalog_name}.{self.schema_name}"
|
|
@@ -408,9 +416,45 @@ class SchemaModel(BaseModel, HasFullName):
|
|
|
408
416
|
|
|
409
417
|
|
|
410
418
|
class DatabricksAppModel(IsDatabricksResource, HasFullName):
|
|
419
|
+
"""
|
|
420
|
+
Configuration for a Databricks App resource.
|
|
421
|
+
|
|
422
|
+
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
|
|
424
|
+
`apps.get(name)` and returning the app's URL.
|
|
425
|
+
|
|
426
|
+
Example:
|
|
427
|
+
```yaml
|
|
428
|
+
resources:
|
|
429
|
+
apps:
|
|
430
|
+
my_app:
|
|
431
|
+
name: my-databricks-app
|
|
432
|
+
```
|
|
433
|
+
"""
|
|
434
|
+
|
|
411
435
|
model_config = ConfigDict(use_enum_values=True, extra="forbid")
|
|
412
436
|
name: str
|
|
413
|
-
|
|
437
|
+
"""The unique instance name of the Databricks App in the workspace."""
|
|
438
|
+
|
|
439
|
+
@property
|
|
440
|
+
def url(self) -> str:
|
|
441
|
+
"""
|
|
442
|
+
Retrieve the URL of the Databricks App from the workspace.
|
|
443
|
+
|
|
444
|
+
Returns:
|
|
445
|
+
The URL of the deployed Databricks App.
|
|
446
|
+
|
|
447
|
+
Raises:
|
|
448
|
+
RuntimeError: If the app is not found or URL is not available.
|
|
449
|
+
"""
|
|
450
|
+
app: App = self.workspace_client.apps.get(self.name)
|
|
451
|
+
if app.url is None:
|
|
452
|
+
raise RuntimeError(
|
|
453
|
+
f"Databricks App '{self.name}' does not have a URL. "
|
|
454
|
+
"The app may not be deployed yet."
|
|
455
|
+
)
|
|
456
|
+
return app.url
|
|
457
|
+
|
|
414
458
|
|
|
415
459
|
@property
|
|
416
460
|
def full_name(self) -> str:
|
|
@@ -432,7 +476,7 @@ class TableModel(IsDatabricksResource, HasFullName):
|
|
|
432
476
|
name: Optional[str] = None
|
|
433
477
|
|
|
434
478
|
@model_validator(mode="after")
|
|
435
|
-
def validate_name_or_schema_required(self) ->
|
|
479
|
+
def validate_name_or_schema_required(self) -> Self:
|
|
436
480
|
if not self.name and not self.schema_model:
|
|
437
481
|
raise ValueError(
|
|
438
482
|
"Either 'name' or 'schema_model' must be provided for TableModel"
|
|
@@ -998,7 +1042,7 @@ class VolumePathModel(BaseModel, HasFullName):
|
|
|
998
1042
|
path: Optional[str] = None
|
|
999
1043
|
|
|
1000
1044
|
@model_validator(mode="after")
|
|
1001
|
-
def validate_path_or_volume(self) ->
|
|
1045
|
+
def validate_path_or_volume(self) -> Self:
|
|
1002
1046
|
if not self.volume and not self.path:
|
|
1003
1047
|
raise ValueError("Either 'volume' or 'path' must be provided")
|
|
1004
1048
|
return self
|
|
@@ -1840,11 +1884,32 @@ class McpFunctionModel(BaseFunctionModel, IsDatabricksResource):
|
|
|
1840
1884
|
headers: dict[str, AnyVariable] = Field(default_factory=dict)
|
|
1841
1885
|
args: list[str] = Field(default_factory=list)
|
|
1842
1886
|
# MCP-specific fields
|
|
1887
|
+
app: Optional[DatabricksAppModel] = None
|
|
1843
1888
|
connection: Optional[ConnectionModel] = None
|
|
1844
1889
|
functions: Optional[SchemaModel] = None
|
|
1845
1890
|
genie_room: Optional[GenieRoomModel] = None
|
|
1846
1891
|
sql: Optional[bool] = None
|
|
1847
1892
|
vector_search: Optional[VectorStoreModel] = None
|
|
1893
|
+
# Tool filtering
|
|
1894
|
+
include_tools: Optional[list[str]] = Field(
|
|
1895
|
+
default=None,
|
|
1896
|
+
description=(
|
|
1897
|
+
"Optional list of tool names or glob patterns to include from the MCP server. "
|
|
1898
|
+
"If specified, only tools matching these patterns will be loaded. "
|
|
1899
|
+
"Supports glob patterns: * (any chars), ? (single char), [abc] (char set). "
|
|
1900
|
+
"Examples: ['execute_query', 'list_*', 'get_?_data']"
|
|
1901
|
+
),
|
|
1902
|
+
)
|
|
1903
|
+
exclude_tools: Optional[list[str]] = Field(
|
|
1904
|
+
default=None,
|
|
1905
|
+
description=(
|
|
1906
|
+
"Optional list of tool names or glob patterns to exclude from the MCP server. "
|
|
1907
|
+
"Tools matching these patterns will not be loaded. "
|
|
1908
|
+
"Takes precedence over include_tools. "
|
|
1909
|
+
"Supports glob patterns: * (any chars), ? (single char), [abc] (char set). "
|
|
1910
|
+
"Examples: ['drop_*', 'delete_*', 'execute_ddl']"
|
|
1911
|
+
),
|
|
1912
|
+
)
|
|
1848
1913
|
|
|
1849
1914
|
@property
|
|
1850
1915
|
def api_scopes(self) -> Sequence[str]:
|
|
@@ -1907,6 +1972,7 @@ class McpFunctionModel(BaseFunctionModel, IsDatabricksResource):
|
|
|
1907
1972
|
|
|
1908
1973
|
Returns the URL based on the configured source:
|
|
1909
1974
|
- If url is set, returns it directly
|
|
1975
|
+
- If app is set, retrieves URL from Databricks App via workspace client
|
|
1910
1976
|
- If connection is set, constructs URL from connection
|
|
1911
1977
|
- If genie_room is set, constructs Genie MCP URL
|
|
1912
1978
|
- If sql is set, constructs DBSQL MCP URL (serverless)
|
|
@@ -1919,6 +1985,7 @@ class McpFunctionModel(BaseFunctionModel, IsDatabricksResource):
|
|
|
1919
1985
|
- Vector Search: https://{host}/api/2.0/mcp/vector-search/{catalog}/{schema}
|
|
1920
1986
|
- UC Functions: https://{host}/api/2.0/mcp/functions/{catalog}/{schema}
|
|
1921
1987
|
- Connection: https://{host}/api/2.0/mcp/external/{connection_name}
|
|
1988
|
+
- Databricks App: Retrieved dynamically from workspace
|
|
1922
1989
|
"""
|
|
1923
1990
|
# Direct URL provided
|
|
1924
1991
|
if self.url:
|
|
@@ -1940,6 +2007,10 @@ class McpFunctionModel(BaseFunctionModel, IsDatabricksResource):
|
|
|
1940
2007
|
# DBSQL MCP server (serverless, workspace-level)
|
|
1941
2008
|
if self.sql:
|
|
1942
2009
|
return f"{workspace_host}/api/2.0/mcp/sql"
|
|
2010
|
+
|
|
2011
|
+
# Databricks App
|
|
2012
|
+
if self.app:
|
|
2013
|
+
return self.app.url
|
|
1943
2014
|
|
|
1944
2015
|
# Vector Search
|
|
1945
2016
|
if self.vector_search:
|
|
@@ -1950,33 +2021,35 @@ class McpFunctionModel(BaseFunctionModel, IsDatabricksResource):
|
|
|
1950
2021
|
raise ValueError(
|
|
1951
2022
|
"vector_search must have an index with a schema (catalog/schema) configured"
|
|
1952
2023
|
)
|
|
1953
|
-
catalog: str = self.vector_search.index.schema_model.catalog_name
|
|
1954
|
-
schema: str = self.vector_search.index.schema_model.schema_name
|
|
2024
|
+
catalog: str = value_of(self.vector_search.index.schema_model.catalog_name)
|
|
2025
|
+
schema: str = value_of(self.vector_search.index.schema_model.schema_name)
|
|
1955
2026
|
return f"{workspace_host}/api/2.0/mcp/vector-search/{catalog}/{schema}"
|
|
1956
2027
|
|
|
1957
2028
|
# UC Functions MCP server
|
|
1958
2029
|
if self.functions:
|
|
1959
|
-
catalog: str = self.functions.catalog_name
|
|
1960
|
-
schema: str = self.functions.schema_name
|
|
2030
|
+
catalog: str = value_of(self.functions.catalog_name)
|
|
2031
|
+
schema: str = value_of(self.functions.schema_name)
|
|
1961
2032
|
return f"{workspace_host}/api/2.0/mcp/functions/{catalog}/{schema}"
|
|
1962
2033
|
|
|
1963
2034
|
raise ValueError(
|
|
1964
|
-
"No URL source configured. Provide one of: url, connection, genie_room, "
|
|
2035
|
+
"No URL source configured. Provide one of: url, app, connection, genie_room, "
|
|
1965
2036
|
"sql, vector_search, or functions"
|
|
1966
2037
|
)
|
|
1967
2038
|
|
|
1968
2039
|
@field_serializer("transport")
|
|
1969
|
-
def serialize_transport(self, value) -> str:
|
|
2040
|
+
def serialize_transport(self, value: TransportType) -> str:
|
|
2041
|
+
"""Serialize transport enum to string."""
|
|
1970
2042
|
if isinstance(value, TransportType):
|
|
1971
2043
|
return value.value
|
|
1972
2044
|
return str(value)
|
|
1973
2045
|
|
|
1974
2046
|
@model_validator(mode="after")
|
|
1975
|
-
def validate_mutually_exclusive(self) ->
|
|
2047
|
+
def validate_mutually_exclusive(self) -> Self:
|
|
1976
2048
|
"""Validate that exactly one URL source is provided."""
|
|
1977
2049
|
# Count how many URL sources are provided
|
|
1978
2050
|
url_sources: list[tuple[str, Any]] = [
|
|
1979
2051
|
("url", self.url),
|
|
2052
|
+
("app", self.app),
|
|
1980
2053
|
("connection", self.connection),
|
|
1981
2054
|
("genie_room", self.genie_room),
|
|
1982
2055
|
("sql", self.sql),
|
|
@@ -1992,13 +2065,13 @@ class McpFunctionModel(BaseFunctionModel, IsDatabricksResource):
|
|
|
1992
2065
|
if len(provided_sources) == 0:
|
|
1993
2066
|
raise ValueError(
|
|
1994
2067
|
"For STREAMABLE_HTTP transport, exactly one of the following must be provided: "
|
|
1995
|
-
"url, connection, genie_room, sql, vector_search, or functions"
|
|
2068
|
+
"url, app, connection, genie_room, sql, vector_search, or functions"
|
|
1996
2069
|
)
|
|
1997
2070
|
if len(provided_sources) > 1:
|
|
1998
2071
|
raise ValueError(
|
|
1999
2072
|
f"For STREAMABLE_HTTP transport, only one URL source can be provided. "
|
|
2000
2073
|
f"Found: {', '.join(provided_sources)}. "
|
|
2001
|
-
f"Please provide only one of: url, connection, genie_room, sql, vector_search, or functions"
|
|
2074
|
+
f"Please provide only one of: url, app, connection, genie_room, sql, vector_search, or functions"
|
|
2002
2075
|
)
|
|
2003
2076
|
|
|
2004
2077
|
if self.transport == TransportType.STDIO:
|
|
@@ -2010,14 +2083,41 @@ class McpFunctionModel(BaseFunctionModel, IsDatabricksResource):
|
|
|
2010
2083
|
return self
|
|
2011
2084
|
|
|
2012
2085
|
@model_validator(mode="after")
|
|
2013
|
-
def update_url(self) ->
|
|
2014
|
-
|
|
2086
|
+
def update_url(self) -> Self:
|
|
2087
|
+
"""Resolve AnyVariable to concrete value for URL."""
|
|
2088
|
+
if self.url is not None:
|
|
2089
|
+
resolved_value: Any = value_of(self.url)
|
|
2090
|
+
# Cast to string since URL must be a string
|
|
2091
|
+
self.url = str(resolved_value) if resolved_value else None
|
|
2015
2092
|
return self
|
|
2016
2093
|
|
|
2017
2094
|
@model_validator(mode="after")
|
|
2018
|
-
def update_headers(self) ->
|
|
2095
|
+
def update_headers(self) -> Self:
|
|
2096
|
+
"""Resolve AnyVariable to concrete values for headers."""
|
|
2019
2097
|
for key, value in self.headers.items():
|
|
2020
|
-
|
|
2098
|
+
resolved_value: Any = value_of(value)
|
|
2099
|
+
# Headers must be strings
|
|
2100
|
+
self.headers[key] = str(resolved_value) if resolved_value else ""
|
|
2101
|
+
return self
|
|
2102
|
+
|
|
2103
|
+
@model_validator(mode="after")
|
|
2104
|
+
def validate_tool_filters(self) -> Self:
|
|
2105
|
+
"""Validate tool filter configuration."""
|
|
2106
|
+
from loguru import logger
|
|
2107
|
+
|
|
2108
|
+
# Warn if both are empty lists (explicit but pointless)
|
|
2109
|
+
if self.include_tools is not None and len(self.include_tools) == 0:
|
|
2110
|
+
logger.warning(
|
|
2111
|
+
"include_tools is empty list - no tools will be loaded. "
|
|
2112
|
+
"Remove field to load all tools."
|
|
2113
|
+
)
|
|
2114
|
+
|
|
2115
|
+
if self.exclude_tools is not None and len(self.exclude_tools) == 0:
|
|
2116
|
+
logger.warning(
|
|
2117
|
+
"exclude_tools is empty list - has no effect. "
|
|
2118
|
+
"Remove field or add patterns."
|
|
2119
|
+
)
|
|
2120
|
+
|
|
2021
2121
|
return self
|
|
2022
2122
|
|
|
2023
2123
|
def as_tools(self, **kwargs: Any) -> Sequence[RunnableLike]:
|
dao_ai/middleware/__init__.py
CHANGED
|
@@ -6,6 +6,7 @@ from langchain.agents.middleware import (
|
|
|
6
6
|
ClearToolUsesEdit,
|
|
7
7
|
ContextEditingMiddleware,
|
|
8
8
|
HumanInTheLoopMiddleware,
|
|
9
|
+
LLMToolSelectorMiddleware,
|
|
9
10
|
ModelCallLimitMiddleware,
|
|
10
11
|
ModelRetryMiddleware,
|
|
11
12
|
PIIMiddleware,
|
|
@@ -82,6 +83,7 @@ from dao_ai.middleware.summarization import (
|
|
|
82
83
|
)
|
|
83
84
|
from dao_ai.middleware.tool_call_limit import create_tool_call_limit_middleware
|
|
84
85
|
from dao_ai.middleware.tool_retry import create_tool_retry_middleware
|
|
86
|
+
from dao_ai.middleware.tool_selector import create_llm_tool_selector_middleware
|
|
85
87
|
|
|
86
88
|
__all__ = [
|
|
87
89
|
# Base class (from LangChain)
|
|
@@ -105,6 +107,7 @@ __all__ = [
|
|
|
105
107
|
"ModelCallLimitMiddleware",
|
|
106
108
|
"ToolRetryMiddleware",
|
|
107
109
|
"ModelRetryMiddleware",
|
|
110
|
+
"LLMToolSelectorMiddleware",
|
|
108
111
|
"ContextEditingMiddleware",
|
|
109
112
|
"ClearToolUsesEdit",
|
|
110
113
|
"PIIMiddleware",
|
|
@@ -150,6 +153,8 @@ __all__ = [
|
|
|
150
153
|
"create_model_call_limit_middleware",
|
|
151
154
|
"create_tool_retry_middleware",
|
|
152
155
|
"create_model_retry_middleware",
|
|
156
|
+
# Tool selection middleware factory functions
|
|
157
|
+
"create_llm_tool_selector_middleware",
|
|
153
158
|
# Context editing middleware factory functions
|
|
154
159
|
"create_context_editing_middleware",
|
|
155
160
|
"create_clear_tool_uses_edit",
|