dao-ai 0.1.5__py3-none-any.whl → 0.1.6__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 +56 -3
- 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.6.dist-info}/METADATA +1 -1
- {dao_ai-0.1.5.dist-info → dao_ai-0.1.6.dist-info}/RECORD +12 -11
- {dao_ai-0.1.5.dist-info → dao_ai-0.1.6.dist-info}/WHEEL +0 -0
- {dao_ai-0.1.5.dist-info → dao_ai-0.1.6.dist-info}/entry_points.txt +0 -0
- {dao_ai-0.1.5.dist-info → dao_ai-0.1.6.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
|
@@ -391,10 +391,17 @@ class PermissionModel(BaseModel):
|
|
|
391
391
|
|
|
392
392
|
class SchemaModel(BaseModel, HasFullName):
|
|
393
393
|
model_config = ConfigDict(use_enum_values=True, extra="forbid")
|
|
394
|
-
catalog_name:
|
|
395
|
-
schema_name:
|
|
394
|
+
catalog_name: AnyVariable
|
|
395
|
+
schema_name: AnyVariable
|
|
396
396
|
permissions: Optional[list[PermissionModel]] = Field(default_factory=list)
|
|
397
397
|
|
|
398
|
+
@model_validator(mode="after")
|
|
399
|
+
def resolve_variables(self) -> Self:
|
|
400
|
+
"""Resolve AnyVariable fields to their actual string values."""
|
|
401
|
+
self.catalog_name = value_of(self.catalog_name)
|
|
402
|
+
self.schema_name = value_of(self.schema_name)
|
|
403
|
+
return self
|
|
404
|
+
|
|
398
405
|
@property
|
|
399
406
|
def full_name(self) -> str:
|
|
400
407
|
return f"{self.catalog_name}.{self.schema_name}"
|
|
@@ -410,7 +417,13 @@ class SchemaModel(BaseModel, HasFullName):
|
|
|
410
417
|
class DatabricksAppModel(IsDatabricksResource, HasFullName):
|
|
411
418
|
model_config = ConfigDict(use_enum_values=True, extra="forbid")
|
|
412
419
|
name: str
|
|
413
|
-
url:
|
|
420
|
+
url: AnyVariable
|
|
421
|
+
|
|
422
|
+
@model_validator(mode="after")
|
|
423
|
+
def resolve_variables(self) -> Self:
|
|
424
|
+
"""Resolve AnyVariable fields to their actual string values."""
|
|
425
|
+
self.url = value_of(self.url)
|
|
426
|
+
return self
|
|
414
427
|
|
|
415
428
|
@property
|
|
416
429
|
def full_name(self) -> str:
|
|
@@ -1845,6 +1858,26 @@ class McpFunctionModel(BaseFunctionModel, IsDatabricksResource):
|
|
|
1845
1858
|
genie_room: Optional[GenieRoomModel] = None
|
|
1846
1859
|
sql: Optional[bool] = None
|
|
1847
1860
|
vector_search: Optional[VectorStoreModel] = None
|
|
1861
|
+
# Tool filtering
|
|
1862
|
+
include_tools: Optional[list[str]] = Field(
|
|
1863
|
+
default=None,
|
|
1864
|
+
description=(
|
|
1865
|
+
"Optional list of tool names or glob patterns to include from the MCP server. "
|
|
1866
|
+
"If specified, only tools matching these patterns will be loaded. "
|
|
1867
|
+
"Supports glob patterns: * (any chars), ? (single char), [abc] (char set). "
|
|
1868
|
+
"Examples: ['execute_query', 'list_*', 'get_?_data']"
|
|
1869
|
+
),
|
|
1870
|
+
)
|
|
1871
|
+
exclude_tools: Optional[list[str]] = Field(
|
|
1872
|
+
default=None,
|
|
1873
|
+
description=(
|
|
1874
|
+
"Optional list of tool names or glob patterns to exclude from the MCP server. "
|
|
1875
|
+
"Tools matching these patterns will not be loaded. "
|
|
1876
|
+
"Takes precedence over include_tools. "
|
|
1877
|
+
"Supports glob patterns: * (any chars), ? (single char), [abc] (char set). "
|
|
1878
|
+
"Examples: ['drop_*', 'delete_*', 'execute_ddl']"
|
|
1879
|
+
),
|
|
1880
|
+
)
|
|
1848
1881
|
|
|
1849
1882
|
@property
|
|
1850
1883
|
def api_scopes(self) -> Sequence[str]:
|
|
@@ -2020,6 +2053,26 @@ class McpFunctionModel(BaseFunctionModel, IsDatabricksResource):
|
|
|
2020
2053
|
self.headers[key] = value_of(value)
|
|
2021
2054
|
return self
|
|
2022
2055
|
|
|
2056
|
+
@model_validator(mode="after")
|
|
2057
|
+
def validate_tool_filters(self) -> "McpFunctionModel":
|
|
2058
|
+
"""Validate tool filter configuration."""
|
|
2059
|
+
from loguru import logger
|
|
2060
|
+
|
|
2061
|
+
# Warn if both are empty lists (explicit but pointless)
|
|
2062
|
+
if self.include_tools is not None and len(self.include_tools) == 0:
|
|
2063
|
+
logger.warning(
|
|
2064
|
+
"include_tools is empty list - no tools will be loaded. "
|
|
2065
|
+
"Remove field to load all tools."
|
|
2066
|
+
)
|
|
2067
|
+
|
|
2068
|
+
if self.exclude_tools is not None and len(self.exclude_tools) == 0:
|
|
2069
|
+
logger.warning(
|
|
2070
|
+
"exclude_tools is empty list - has no effect. "
|
|
2071
|
+
"Remove field or add patterns."
|
|
2072
|
+
)
|
|
2073
|
+
|
|
2074
|
+
return self
|
|
2075
|
+
|
|
2023
2076
|
def as_tools(self, **kwargs: Any) -> Sequence[RunnableLike]:
|
|
2024
2077
|
from dao_ai.tools import create_mcp_tools
|
|
2025
2078
|
|
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",
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tool selector middleware for intelligently filtering tools before LLM calls.
|
|
3
|
+
|
|
4
|
+
This middleware uses an LLM to select relevant tools from a large set, improving
|
|
5
|
+
performance and accuracy by reducing context size and improving focus.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from langchain.agents.middleware import LLMToolSelectorMiddleware
|
|
13
|
+
from langchain_core.language_models import LanguageModelLike
|
|
14
|
+
from loguru import logger
|
|
15
|
+
|
|
16
|
+
from dao_ai.config import ToolModel
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def create_llm_tool_selector_middleware(
|
|
20
|
+
model: LanguageModelLike,
|
|
21
|
+
max_tools: int = 3,
|
|
22
|
+
always_include: list[str | ToolModel | dict[str, Any]] | None = None,
|
|
23
|
+
) -> LLMToolSelectorMiddleware:
|
|
24
|
+
"""
|
|
25
|
+
Create an LLMToolSelectorMiddleware for intelligent tool selection.
|
|
26
|
+
|
|
27
|
+
Uses an LLM to analyze the current query and select the most relevant tools
|
|
28
|
+
before calling the main model. This is particularly useful for agents with
|
|
29
|
+
many tools (10+) where most aren't relevant for any given query.
|
|
30
|
+
|
|
31
|
+
Benefits:
|
|
32
|
+
- Reduces token usage by filtering irrelevant tools
|
|
33
|
+
- Improves model focus and accuracy
|
|
34
|
+
- Optimizes cost for agents with large tool sets
|
|
35
|
+
- Maintains context window efficiency
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
model: The LLM to use for tool selection. Typically a smaller, faster
|
|
39
|
+
model like "gpt-4o-mini" or similar.
|
|
40
|
+
max_tools: Maximum number of tools to select for each query.
|
|
41
|
+
Default 3. Adjust based on your use case - higher values
|
|
42
|
+
increase context but improve tool coverage.
|
|
43
|
+
always_include: List of tools that should always be included regardless
|
|
44
|
+
of the LLM's selection. Can be:
|
|
45
|
+
- str: Tool name
|
|
46
|
+
- ToolModel: Full tool configuration
|
|
47
|
+
- dict: Tool configuration dictionary
|
|
48
|
+
Use this for critical tools that should always be available.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
LLMToolSelectorMiddleware configured with the specified parameters
|
|
52
|
+
|
|
53
|
+
Example:
|
|
54
|
+
from dao_ai.middleware import create_llm_tool_selector_middleware
|
|
55
|
+
from dao_ai.llms import create_llm
|
|
56
|
+
|
|
57
|
+
# Use a fast, cheap model for tool selection
|
|
58
|
+
selector_llm = create_llm("databricks-gpt-4o-mini")
|
|
59
|
+
|
|
60
|
+
middleware = create_llm_tool_selector_middleware(
|
|
61
|
+
model=selector_llm,
|
|
62
|
+
max_tools=3,
|
|
63
|
+
always_include=["search_web"], # Always include search
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
Use Cases:
|
|
67
|
+
- Large tool sets (10+ tools) where most are specialized
|
|
68
|
+
- Cost optimization by reducing tokens in main model calls
|
|
69
|
+
- Improved accuracy by reducing tool confusion
|
|
70
|
+
- Dynamic tool filtering based on query relevance
|
|
71
|
+
|
|
72
|
+
Note:
|
|
73
|
+
The selector model makes an additional LLM call for each agent turn.
|
|
74
|
+
Choose a fast, inexpensive model to minimize latency and cost overhead.
|
|
75
|
+
"""
|
|
76
|
+
# Extract tool names from always_include
|
|
77
|
+
always_include_names: list[str] = []
|
|
78
|
+
if always_include:
|
|
79
|
+
always_include_names = _resolve_tool_names(always_include)
|
|
80
|
+
|
|
81
|
+
logger.debug(
|
|
82
|
+
"Creating LLM tool selector middleware",
|
|
83
|
+
max_tools=max_tools,
|
|
84
|
+
always_include_count=len(always_include_names),
|
|
85
|
+
always_include=always_include_names,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return LLMToolSelectorMiddleware(
|
|
89
|
+
model=model,
|
|
90
|
+
max_tools=max_tools,
|
|
91
|
+
always_include=always_include_names if always_include_names else None,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _resolve_tool_names(tools: list[str | ToolModel | dict[str, Any]]) -> list[str]:
|
|
96
|
+
"""
|
|
97
|
+
Extract tool names from a list of tool specifications.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
tools: List of tool specifications (strings, ToolModels, or dicts)
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
List of tool names as strings
|
|
104
|
+
"""
|
|
105
|
+
names: list[str] = []
|
|
106
|
+
|
|
107
|
+
for tool_spec in tools:
|
|
108
|
+
if isinstance(tool_spec, str):
|
|
109
|
+
# Simple string tool name
|
|
110
|
+
names.append(tool_spec)
|
|
111
|
+
elif isinstance(tool_spec, ToolModel):
|
|
112
|
+
# ToolModel - use its name
|
|
113
|
+
names.append(tool_spec.name)
|
|
114
|
+
elif isinstance(tool_spec, dict):
|
|
115
|
+
# Dictionary - try to extract name
|
|
116
|
+
if "name" in tool_spec:
|
|
117
|
+
names.append(tool_spec["name"])
|
|
118
|
+
else:
|
|
119
|
+
logger.warning(
|
|
120
|
+
"Tool dict missing 'name' field, skipping",
|
|
121
|
+
tool_spec=tool_spec,
|
|
122
|
+
)
|
|
123
|
+
else:
|
|
124
|
+
logger.warning(
|
|
125
|
+
"Unknown tool specification type, skipping",
|
|
126
|
+
tool_spec_type=type(tool_spec).__name__,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
return names
|
dao_ai/prompts.py
CHANGED
|
@@ -61,19 +61,14 @@ def make_prompt(
|
|
|
61
61
|
@dynamic_prompt
|
|
62
62
|
def dynamic_system_prompt(request: ModelRequest) -> str:
|
|
63
63
|
"""Generate dynamic system prompt based on runtime context."""
|
|
64
|
-
#
|
|
64
|
+
# Initialize parameters for template variables
|
|
65
65
|
params: dict[str, Any] = {
|
|
66
66
|
input_variable: "" for input_variable in prompt_template.input_variables
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
#
|
|
69
|
+
# Apply context fields as template parameters
|
|
70
70
|
context: Context = request.runtime.context
|
|
71
71
|
if context:
|
|
72
|
-
if context.user_id and "user_id" in params:
|
|
73
|
-
params["user_id"] = context.user_id
|
|
74
|
-
if context.thread_id and "thread_id" in params:
|
|
75
|
-
params["thread_id"] = context.thread_id
|
|
76
|
-
# Apply all context fields as template parameters
|
|
77
72
|
context_dict = context.model_dump()
|
|
78
73
|
for key, value in context_dict.items():
|
|
79
74
|
if key in params and value is not None:
|
|
@@ -89,56 +84,3 @@ def make_prompt(
|
|
|
89
84
|
return formatted_prompt
|
|
90
85
|
|
|
91
86
|
return dynamic_system_prompt
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def create_prompt_middleware(
|
|
95
|
-
base_system_prompt: Optional[str | PromptModel],
|
|
96
|
-
) -> AgentMiddleware | None:
|
|
97
|
-
"""
|
|
98
|
-
Create a dynamic prompt middleware from configuration.
|
|
99
|
-
|
|
100
|
-
This always returns an AgentMiddleware suitable for use with
|
|
101
|
-
LangChain v1's middleware system.
|
|
102
|
-
|
|
103
|
-
Args:
|
|
104
|
-
base_system_prompt: The system prompt string or PromptModel
|
|
105
|
-
|
|
106
|
-
Returns:
|
|
107
|
-
An AgentMiddleware created by @dynamic_prompt, or None if no prompt
|
|
108
|
-
"""
|
|
109
|
-
if not base_system_prompt:
|
|
110
|
-
return None
|
|
111
|
-
|
|
112
|
-
# Extract template string from PromptModel or use string directly
|
|
113
|
-
template_str: str
|
|
114
|
-
if isinstance(base_system_prompt, PromptModel):
|
|
115
|
-
template_str = base_system_prompt.template
|
|
116
|
-
else:
|
|
117
|
-
template_str = base_system_prompt
|
|
118
|
-
|
|
119
|
-
prompt_template: PromptTemplate = PromptTemplate.from_template(template_str)
|
|
120
|
-
|
|
121
|
-
@dynamic_prompt
|
|
122
|
-
def prompt_middleware(request: ModelRequest) -> str:
|
|
123
|
-
"""Generate system prompt based on runtime context."""
|
|
124
|
-
# Get parameters from runtime context
|
|
125
|
-
params: dict[str, Any] = {
|
|
126
|
-
input_variable: "" for input_variable in prompt_template.input_variables
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
# Access context from runtime
|
|
130
|
-
context: Context = request.runtime.context
|
|
131
|
-
if context:
|
|
132
|
-
# Apply all context fields as template parameters
|
|
133
|
-
context_dict = context.model_dump()
|
|
134
|
-
for key, value in context_dict.items():
|
|
135
|
-
if key in params and value is not None:
|
|
136
|
-
params[key] = value
|
|
137
|
-
|
|
138
|
-
# Format the prompt
|
|
139
|
-
formatted_prompt: str = prompt_template.format(**params)
|
|
140
|
-
logger.trace("Formatted dynamic prompt with context")
|
|
141
|
-
|
|
142
|
-
return formatted_prompt
|
|
143
|
-
|
|
144
|
-
return prompt_middleware
|
dao_ai/tools/__init__.py
CHANGED
|
@@ -4,7 +4,7 @@ from dao_ai.tools.agent import create_agent_endpoint_tool
|
|
|
4
4
|
from dao_ai.tools.core import create_tools, say_hello_tool
|
|
5
5
|
from dao_ai.tools.email import create_send_email_tool
|
|
6
6
|
from dao_ai.tools.genie import create_genie_tool
|
|
7
|
-
from dao_ai.tools.mcp import create_mcp_tools
|
|
7
|
+
from dao_ai.tools.mcp import MCPToolInfo, create_mcp_tools, list_mcp_tools
|
|
8
8
|
from dao_ai.tools.memory import create_search_memory_tool
|
|
9
9
|
from dao_ai.tools.python import create_factory_tool, create_python_tool
|
|
10
10
|
from dao_ai.tools.search import create_search_tool
|
|
@@ -30,6 +30,8 @@ __all__ = [
|
|
|
30
30
|
"create_genie_tool",
|
|
31
31
|
"create_hooks",
|
|
32
32
|
"create_mcp_tools",
|
|
33
|
+
"list_mcp_tools",
|
|
34
|
+
"MCPToolInfo",
|
|
33
35
|
"create_python_tool",
|
|
34
36
|
"create_search_memory_tool",
|
|
35
37
|
"create_search_tool",
|
dao_ai/tools/mcp.py
CHANGED
|
@@ -7,10 +7,16 @@ MCP SDK and langchain-mcp-adapters library.
|
|
|
7
7
|
For compatibility with Databricks APIs, we use manual tool wrappers
|
|
8
8
|
that give us full control over the response format.
|
|
9
9
|
|
|
10
|
+
Public API:
|
|
11
|
+
- list_mcp_tools(): List available tools from an MCP server (for discovery/UI)
|
|
12
|
+
- create_mcp_tools(): Create LangChain tools for agent execution
|
|
13
|
+
|
|
10
14
|
Reference: https://docs.langchain.com/oss/python/langchain/mcp
|
|
11
15
|
"""
|
|
12
16
|
|
|
13
17
|
import asyncio
|
|
18
|
+
import fnmatch
|
|
19
|
+
from dataclasses import dataclass
|
|
14
20
|
from typing import Any, Sequence
|
|
15
21
|
|
|
16
22
|
from langchain_core.runnables.base import RunnableLike
|
|
@@ -26,6 +32,117 @@ from dao_ai.config import (
|
|
|
26
32
|
)
|
|
27
33
|
|
|
28
34
|
|
|
35
|
+
@dataclass
|
|
36
|
+
class MCPToolInfo:
|
|
37
|
+
"""
|
|
38
|
+
Information about an MCP tool for display and selection.
|
|
39
|
+
|
|
40
|
+
This is a simplified representation of an MCP tool that contains
|
|
41
|
+
only the information needed for UI display and tool selection.
|
|
42
|
+
It's designed to be easily serializable for use in web UIs.
|
|
43
|
+
|
|
44
|
+
Attributes:
|
|
45
|
+
name: The unique identifier/name of the tool
|
|
46
|
+
description: Human-readable description of what the tool does
|
|
47
|
+
input_schema: JSON Schema describing the tool's input parameters
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
name: str
|
|
51
|
+
description: str | None
|
|
52
|
+
input_schema: dict[str, Any]
|
|
53
|
+
|
|
54
|
+
def to_dict(self) -> dict[str, Any]:
|
|
55
|
+
"""Convert to dictionary for JSON serialization."""
|
|
56
|
+
return {
|
|
57
|
+
"name": self.name,
|
|
58
|
+
"description": self.description,
|
|
59
|
+
"input_schema": self.input_schema,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _matches_pattern(tool_name: str, patterns: list[str]) -> bool:
|
|
64
|
+
"""
|
|
65
|
+
Check if tool name matches any of the provided patterns.
|
|
66
|
+
|
|
67
|
+
Supports glob patterns:
|
|
68
|
+
- * matches any characters
|
|
69
|
+
- ? matches single character
|
|
70
|
+
- [abc] matches any char in set
|
|
71
|
+
- [!abc] matches any char NOT in set
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
tool_name: Name of the tool to check
|
|
75
|
+
patterns: List of exact names or glob patterns
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
True if tool name matches any pattern
|
|
79
|
+
|
|
80
|
+
Examples:
|
|
81
|
+
>>> _matches_pattern("query_sales", ["query_*"])
|
|
82
|
+
True
|
|
83
|
+
>>> _matches_pattern("list_tables", ["query_*"])
|
|
84
|
+
False
|
|
85
|
+
>>> _matches_pattern("tool_a", ["tool_?"])
|
|
86
|
+
True
|
|
87
|
+
"""
|
|
88
|
+
for pattern in patterns:
|
|
89
|
+
if fnmatch.fnmatch(tool_name, pattern):
|
|
90
|
+
return True
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _should_include_tool(
|
|
95
|
+
tool_name: str,
|
|
96
|
+
include_tools: list[str] | None,
|
|
97
|
+
exclude_tools: list[str] | None,
|
|
98
|
+
) -> bool:
|
|
99
|
+
"""
|
|
100
|
+
Determine if a tool should be included based on include/exclude filters.
|
|
101
|
+
|
|
102
|
+
Logic:
|
|
103
|
+
1. If exclude_tools specified and tool matches: EXCLUDE (highest priority)
|
|
104
|
+
2. If include_tools specified and tool matches: INCLUDE
|
|
105
|
+
3. If include_tools specified and tool doesn't match: EXCLUDE
|
|
106
|
+
4. If no filters specified: INCLUDE (default)
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
tool_name: Name of the tool
|
|
110
|
+
include_tools: Optional list of tools/patterns to include
|
|
111
|
+
exclude_tools: Optional list of tools/patterns to exclude
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
True if tool should be included
|
|
115
|
+
|
|
116
|
+
Examples:
|
|
117
|
+
>>> _should_include_tool("query_sales", ["query_*"], None)
|
|
118
|
+
True
|
|
119
|
+
>>> _should_include_tool("drop_table", None, ["drop_*"])
|
|
120
|
+
False
|
|
121
|
+
>>> _should_include_tool("query_sales", ["query_*"], ["*_sales"])
|
|
122
|
+
False # exclude takes precedence
|
|
123
|
+
"""
|
|
124
|
+
# Exclude has highest priority
|
|
125
|
+
if exclude_tools and _matches_pattern(tool_name, exclude_tools):
|
|
126
|
+
logger.debug("Tool excluded by exclude_tools", tool_name=tool_name)
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
# If include list exists, tool must match it
|
|
130
|
+
if include_tools:
|
|
131
|
+
if _matches_pattern(tool_name, include_tools):
|
|
132
|
+
logger.debug("Tool included by include_tools", tool_name=tool_name)
|
|
133
|
+
return True
|
|
134
|
+
else:
|
|
135
|
+
logger.debug(
|
|
136
|
+
"Tool not in include_tools",
|
|
137
|
+
tool_name=tool_name,
|
|
138
|
+
include_patterns=include_tools,
|
|
139
|
+
)
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
# Default: include all tools
|
|
143
|
+
return True
|
|
144
|
+
|
|
145
|
+
|
|
29
146
|
def _build_connection_config(
|
|
30
147
|
function: McpFunctionModel,
|
|
31
148
|
) -> dict[str, Any]:
|
|
@@ -124,69 +241,33 @@ def _extract_text_content(result: CallToolResult) -> str:
|
|
|
124
241
|
return "\n".join(text_parts)
|
|
125
242
|
|
|
126
243
|
|
|
127
|
-
def
|
|
128
|
-
function: McpFunctionModel,
|
|
129
|
-
) -> Sequence[RunnableLike]:
|
|
244
|
+
def _fetch_tools_from_server(function: McpFunctionModel) -> list[Tool]:
|
|
130
245
|
"""
|
|
131
|
-
|
|
246
|
+
Fetch raw MCP tools from the server.
|
|
132
247
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
Databricks APIs (which reject extra fields in tool results).
|
|
136
|
-
|
|
137
|
-
Based on: https://docs.databricks.com/aws/en/generative-ai/mcp/external-mcp
|
|
248
|
+
This is the core async operation that connects to the MCP server
|
|
249
|
+
and retrieves the list of available tools.
|
|
138
250
|
|
|
139
251
|
Args:
|
|
140
252
|
function: The MCP function model configuration.
|
|
141
253
|
|
|
142
254
|
Returns:
|
|
143
|
-
|
|
144
|
-
"""
|
|
145
|
-
mcp_url = function.mcp_url
|
|
146
|
-
logger.debug("Creating MCP tools", mcp_url=mcp_url)
|
|
255
|
+
List of raw MCP Tool objects from the server.
|
|
147
256
|
|
|
257
|
+
Raises:
|
|
258
|
+
RuntimeError: If connection to MCP server fails.
|
|
259
|
+
"""
|
|
148
260
|
connection_config = _build_connection_config(function)
|
|
149
|
-
|
|
150
|
-
if function.connection:
|
|
151
|
-
logger.debug(
|
|
152
|
-
"Using UC Connection for MCP",
|
|
153
|
-
connection_name=function.connection.name,
|
|
154
|
-
mcp_url=mcp_url,
|
|
155
|
-
)
|
|
156
|
-
else:
|
|
157
|
-
logger.debug(
|
|
158
|
-
"Using direct connection for MCP",
|
|
159
|
-
transport=function.transport,
|
|
160
|
-
mcp_url=mcp_url,
|
|
161
|
-
)
|
|
162
|
-
|
|
163
|
-
# Create client to list available tools
|
|
164
261
|
client = MultiServerMCPClient({"mcp_function": connection_config})
|
|
165
262
|
|
|
166
|
-
async def
|
|
167
|
-
"""
|
|
263
|
+
async def _list_tools_async() -> list[Tool]:
|
|
264
|
+
"""Async helper to list tools from MCP server."""
|
|
168
265
|
async with client.session("mcp_function") as session:
|
|
169
266
|
result = await session.list_tools()
|
|
170
267
|
return result.tools if hasattr(result, "tools") else list(result)
|
|
171
268
|
|
|
172
269
|
try:
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
# Log discovered tools
|
|
176
|
-
logger.info(
|
|
177
|
-
"Discovered MCP tools",
|
|
178
|
-
tools_count=len(mcp_tools),
|
|
179
|
-
mcp_url=mcp_url,
|
|
180
|
-
)
|
|
181
|
-
for mcp_tool in mcp_tools:
|
|
182
|
-
logger.debug(
|
|
183
|
-
"MCP tool discovered",
|
|
184
|
-
tool_name=mcp_tool.name,
|
|
185
|
-
tool_description=(
|
|
186
|
-
mcp_tool.description[:100] if mcp_tool.description else None
|
|
187
|
-
),
|
|
188
|
-
)
|
|
189
|
-
|
|
270
|
+
return asyncio.run(_list_tools_async())
|
|
190
271
|
except Exception as e:
|
|
191
272
|
if function.connection:
|
|
192
273
|
logger.error(
|
|
@@ -210,6 +291,216 @@ def create_mcp_tools(
|
|
|
210
291
|
f"and URL '{function.url}': {e}"
|
|
211
292
|
) from e
|
|
212
293
|
|
|
294
|
+
|
|
295
|
+
def list_mcp_tools(
|
|
296
|
+
function: McpFunctionModel,
|
|
297
|
+
apply_filters: bool = True,
|
|
298
|
+
) -> list[MCPToolInfo]:
|
|
299
|
+
"""
|
|
300
|
+
List available tools from an MCP server.
|
|
301
|
+
|
|
302
|
+
This function connects to an MCP server and returns information about
|
|
303
|
+
all available tools. It's designed for:
|
|
304
|
+
- Tool discovery and exploration
|
|
305
|
+
- UI-based tool selection (e.g., in DAO AI Builder)
|
|
306
|
+
- Debugging and validation of MCP configurations
|
|
307
|
+
|
|
308
|
+
The returned MCPToolInfo objects contain all information needed to
|
|
309
|
+
display tools in a UI and allow users to select which tools to use.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
function: The MCP function model configuration containing:
|
|
313
|
+
- Connection details (url, connection, headers, etc.)
|
|
314
|
+
- Optional filtering (include_tools, exclude_tools)
|
|
315
|
+
apply_filters: Whether to apply include_tools/exclude_tools filters.
|
|
316
|
+
Set to False to get the complete list of available tools
|
|
317
|
+
regardless of filter configuration. Default True.
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
List of MCPToolInfo objects describing available tools.
|
|
321
|
+
Each contains name, description, and input_schema.
|
|
322
|
+
|
|
323
|
+
Raises:
|
|
324
|
+
RuntimeError: If connection to MCP server fails.
|
|
325
|
+
|
|
326
|
+
Example:
|
|
327
|
+
# List all tools from a DBSQL MCP server
|
|
328
|
+
from dao_ai.config import McpFunctionModel
|
|
329
|
+
from dao_ai.tools.mcp import list_mcp_tools
|
|
330
|
+
|
|
331
|
+
function = McpFunctionModel(sql=True)
|
|
332
|
+
tools = list_mcp_tools(function)
|
|
333
|
+
|
|
334
|
+
for tool in tools:
|
|
335
|
+
print(f"{tool.name}: {tool.description}")
|
|
336
|
+
|
|
337
|
+
# Get unfiltered list (ignore include_tools/exclude_tools)
|
|
338
|
+
all_tools = list_mcp_tools(function, apply_filters=False)
|
|
339
|
+
|
|
340
|
+
Note:
|
|
341
|
+
For creating executable LangChain tools, use create_mcp_tools() instead.
|
|
342
|
+
This function is for discovery/display purposes only.
|
|
343
|
+
"""
|
|
344
|
+
mcp_url = function.mcp_url
|
|
345
|
+
logger.debug("Listing MCP tools", mcp_url=mcp_url, apply_filters=apply_filters)
|
|
346
|
+
|
|
347
|
+
# Log connection type
|
|
348
|
+
if function.connection:
|
|
349
|
+
logger.debug(
|
|
350
|
+
"Using UC Connection for MCP",
|
|
351
|
+
connection_name=function.connection.name,
|
|
352
|
+
mcp_url=mcp_url,
|
|
353
|
+
)
|
|
354
|
+
else:
|
|
355
|
+
logger.debug(
|
|
356
|
+
"Using direct connection for MCP",
|
|
357
|
+
transport=function.transport,
|
|
358
|
+
mcp_url=mcp_url,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
# Fetch tools from server
|
|
362
|
+
mcp_tools: list[Tool] = _fetch_tools_from_server(function)
|
|
363
|
+
|
|
364
|
+
# Log discovered tools
|
|
365
|
+
logger.info(
|
|
366
|
+
"Discovered MCP tools from server",
|
|
367
|
+
tools_count=len(mcp_tools),
|
|
368
|
+
tool_names=[t.name for t in mcp_tools],
|
|
369
|
+
mcp_url=mcp_url,
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
# Apply filtering if requested and configured
|
|
373
|
+
if apply_filters and (function.include_tools or function.exclude_tools):
|
|
374
|
+
original_count = len(mcp_tools)
|
|
375
|
+
mcp_tools = [
|
|
376
|
+
tool
|
|
377
|
+
for tool in mcp_tools
|
|
378
|
+
if _should_include_tool(
|
|
379
|
+
tool.name,
|
|
380
|
+
function.include_tools,
|
|
381
|
+
function.exclude_tools,
|
|
382
|
+
)
|
|
383
|
+
]
|
|
384
|
+
filtered_count = original_count - len(mcp_tools)
|
|
385
|
+
|
|
386
|
+
logger.info(
|
|
387
|
+
"Filtered MCP tools",
|
|
388
|
+
original_count=original_count,
|
|
389
|
+
filtered_count=filtered_count,
|
|
390
|
+
final_count=len(mcp_tools),
|
|
391
|
+
include_patterns=function.include_tools,
|
|
392
|
+
exclude_patterns=function.exclude_tools,
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
# Convert to MCPToolInfo for cleaner API
|
|
396
|
+
tool_infos: list[MCPToolInfo] = []
|
|
397
|
+
for mcp_tool in mcp_tools:
|
|
398
|
+
tool_info = MCPToolInfo(
|
|
399
|
+
name=mcp_tool.name,
|
|
400
|
+
description=mcp_tool.description,
|
|
401
|
+
input_schema=mcp_tool.inputSchema or {},
|
|
402
|
+
)
|
|
403
|
+
tool_infos.append(tool_info)
|
|
404
|
+
|
|
405
|
+
logger.debug(
|
|
406
|
+
"MCP tool available",
|
|
407
|
+
tool_name=mcp_tool.name,
|
|
408
|
+
tool_description=(
|
|
409
|
+
mcp_tool.description[:100] if mcp_tool.description else None
|
|
410
|
+
),
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
return tool_infos
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def create_mcp_tools(
|
|
417
|
+
function: McpFunctionModel,
|
|
418
|
+
) -> Sequence[RunnableLike]:
|
|
419
|
+
"""
|
|
420
|
+
Create executable LangChain tools for invoking Databricks MCP functions.
|
|
421
|
+
|
|
422
|
+
Supports both direct MCP connections and UC Connection-based MCP access.
|
|
423
|
+
Uses manual tool wrappers to ensure response format compatibility with
|
|
424
|
+
Databricks APIs (which reject extra fields in tool results).
|
|
425
|
+
|
|
426
|
+
This function:
|
|
427
|
+
1. Fetches available tools from the MCP server
|
|
428
|
+
2. Applies include_tools/exclude_tools filters
|
|
429
|
+
3. Wraps each tool for LangChain agent execution
|
|
430
|
+
|
|
431
|
+
For tool discovery without creating executable tools, use list_mcp_tools().
|
|
432
|
+
|
|
433
|
+
Based on: https://docs.databricks.com/aws/en/generative-ai/mcp/external-mcp
|
|
434
|
+
|
|
435
|
+
Args:
|
|
436
|
+
function: The MCP function model configuration containing:
|
|
437
|
+
- Connection details (url, connection, headers, etc.)
|
|
438
|
+
- Optional filtering (include_tools, exclude_tools)
|
|
439
|
+
|
|
440
|
+
Returns:
|
|
441
|
+
A sequence of LangChain tools that can be used by agents.
|
|
442
|
+
|
|
443
|
+
Raises:
|
|
444
|
+
RuntimeError: If connection to MCP server fails.
|
|
445
|
+
|
|
446
|
+
Example:
|
|
447
|
+
from dao_ai.config import McpFunctionModel
|
|
448
|
+
from dao_ai.tools.mcp import create_mcp_tools
|
|
449
|
+
|
|
450
|
+
function = McpFunctionModel(sql=True)
|
|
451
|
+
tools = create_mcp_tools(function)
|
|
452
|
+
|
|
453
|
+
# Use tools in an agent
|
|
454
|
+
agent = create_agent(model=model, tools=tools)
|
|
455
|
+
"""
|
|
456
|
+
mcp_url = function.mcp_url
|
|
457
|
+
logger.debug("Creating MCP tools", mcp_url=mcp_url)
|
|
458
|
+
|
|
459
|
+
# Fetch and filter tools using shared logic
|
|
460
|
+
# We need the raw Tool objects here, not MCPToolInfo
|
|
461
|
+
mcp_tools: list[Tool] = _fetch_tools_from_server(function)
|
|
462
|
+
|
|
463
|
+
# Log discovered tools
|
|
464
|
+
logger.info(
|
|
465
|
+
"Discovered MCP tools from server",
|
|
466
|
+
tools_count=len(mcp_tools),
|
|
467
|
+
tool_names=[t.name for t in mcp_tools],
|
|
468
|
+
mcp_url=mcp_url,
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
# Apply filtering if configured
|
|
472
|
+
if function.include_tools or function.exclude_tools:
|
|
473
|
+
original_count = len(mcp_tools)
|
|
474
|
+
mcp_tools = [
|
|
475
|
+
tool
|
|
476
|
+
for tool in mcp_tools
|
|
477
|
+
if _should_include_tool(
|
|
478
|
+
tool.name,
|
|
479
|
+
function.include_tools,
|
|
480
|
+
function.exclude_tools,
|
|
481
|
+
)
|
|
482
|
+
]
|
|
483
|
+
filtered_count = original_count - len(mcp_tools)
|
|
484
|
+
|
|
485
|
+
logger.info(
|
|
486
|
+
"Filtered MCP tools",
|
|
487
|
+
original_count=original_count,
|
|
488
|
+
filtered_count=filtered_count,
|
|
489
|
+
final_count=len(mcp_tools),
|
|
490
|
+
include_patterns=function.include_tools,
|
|
491
|
+
exclude_patterns=function.exclude_tools,
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
# Log final tool list
|
|
495
|
+
for mcp_tool in mcp_tools:
|
|
496
|
+
logger.debug(
|
|
497
|
+
"MCP tool available",
|
|
498
|
+
tool_name=mcp_tool.name,
|
|
499
|
+
tool_description=(
|
|
500
|
+
mcp_tool.description[:100] if mcp_tool.description else None
|
|
501
|
+
),
|
|
502
|
+
)
|
|
503
|
+
|
|
213
504
|
def _create_tool_wrapper(mcp_tool: Tool) -> RunnableLike:
|
|
214
505
|
"""
|
|
215
506
|
Create a LangChain tool wrapper for an MCP tool.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dao-ai
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
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
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
dao_ai/__init__.py,sha256=18P98ExEgUaJ1Byw440Ct1ty59v6nxyWtc5S6Uq2m9Q,1062
|
|
2
2
|
dao_ai/agent_as_code.py,sha256=xIlLDpPVfmDVzLvbdY_V_CrC4Jvj2ItCWJ-NzdrszTo,538
|
|
3
3
|
dao_ai/catalog.py,sha256=sPZpHTD3lPx4EZUtIWeQV7VQM89WJ6YH__wluk1v2lE,4947
|
|
4
|
-
dao_ai/cli.py,sha256=
|
|
5
|
-
dao_ai/config.py,sha256=
|
|
4
|
+
dao_ai/cli.py,sha256=7LGrVDRgSBpznr8c8EksAhzPW_8NJ9h4St3DSpx-0z4,48196
|
|
5
|
+
dao_ai/config.py,sha256=rUm2wg0TPfj6YwzSoNxy6rgHi6GKWxXIRJ3NgGOjB04,123037
|
|
6
6
|
dao_ai/graph.py,sha256=1-uQlo7iXZQTT3uU8aYu0N5rnhw5_g_2YLwVsAs6M-U,1119
|
|
7
7
|
dao_ai/logging.py,sha256=lYy4BmucCHvwW7aI3YQkQXKJtMvtTnPDu9Hnd7_O4oc,1556
|
|
8
8
|
dao_ai/messages.py,sha256=4ZBzO4iFdktGSLrmhHzFjzMIt2tpaL-aQLHOQJysGnY,6959
|
|
9
9
|
dao_ai/models.py,sha256=AwzwTRTNZF-UOh59HsuXEgFk_YH6q6M-mERNDe64Z8k,81783
|
|
10
10
|
dao_ai/nodes.py,sha256=7W6Ek6Uk9-pKa-H06nVCwuDllCrgX02IYy3rHtuL0aM,10777
|
|
11
11
|
dao_ai/optimization.py,sha256=phK6t4wYmWPObCjGUBHdZzsaFXGhQOjhAek2bAEfwXo,22971
|
|
12
|
-
dao_ai/prompts.py,sha256=
|
|
12
|
+
dao_ai/prompts.py,sha256=4cz5bZ7cOzrjyQ8hMp-K4evK6cVYrkGrAGdUl8-KDEM,2784
|
|
13
13
|
dao_ai/state.py,sha256=0wbbzfQmldkCu26gdTE5j0Rl-_pfilza-YIHPbSWlvI,6394
|
|
14
14
|
dao_ai/types.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
15
|
dao_ai/utils.py,sha256=_Urd7Nj2VzrgPKf3NS4E6vt0lWRhEUddBqWN9BksqeE,11543
|
|
@@ -28,7 +28,7 @@ dao_ai/memory/base.py,sha256=99nfr2UZJ4jmfTL_KrqUlRSCoRxzkZyWyx5WqeUoMdQ,338
|
|
|
28
28
|
dao_ai/memory/core.py,sha256=38H-JLIyUrRDIECLvpXK3iJlWG35X97E-DTo_4c3Jzc,6317
|
|
29
29
|
dao_ai/memory/databricks.py,sha256=SM6nwLjhSRJO4hLc3GUuht5YydYtTi3BAOae6jPwTm4,14377
|
|
30
30
|
dao_ai/memory/postgres.py,sha256=q9IIAGs0wuaV-3rUIn4dtzOxbkCCoB-yv1Rtod7ohjI,16467
|
|
31
|
-
dao_ai/middleware/__init__.py,sha256=
|
|
31
|
+
dao_ai/middleware/__init__.py,sha256=Qy8wbvjXF7TrUzi3tWziOwxqsrUcT1rzE3UWd3x5CrU,5108
|
|
32
32
|
dao_ai/middleware/assertions.py,sha256=C1K-TnNZfBEwWouioHCt6c48i1ux9QKfQaX6AzghhgE,27408
|
|
33
33
|
dao_ai/middleware/base.py,sha256=uG2tpdnjL5xY5jCKvb_m3UTBtl4ZC6fJQUkDsQvV8S4,1279
|
|
34
34
|
dao_ai/middleware/context_editing.py,sha256=5rNKqH1phFFQTVW-4nzlVH5cbqomD-HFEIy2Z841D4I,7687
|
|
@@ -42,6 +42,7 @@ dao_ai/middleware/pii.py,sha256=zetfoz1WlJ-V0vjJp37v8NGimXB27EkZfetUHpGCXno,5137
|
|
|
42
42
|
dao_ai/middleware/summarization.py,sha256=gp2s9uc4DEJat-mWjWEzMaR-zAAeUOXYvu5EEYtqae4,7143
|
|
43
43
|
dao_ai/middleware/tool_call_limit.py,sha256=WQ3NmA3pLo-pNPBmwM7KwkYpT1segEnWqkhgW1xNkCE,6321
|
|
44
44
|
dao_ai/middleware/tool_retry.py,sha256=QfJ7yTHneME8VtnA88QcmnjXIegSFeJztyngy49wTgM,5568
|
|
45
|
+
dao_ai/middleware/tool_selector.py,sha256=POj72YdzZEiNGfW4AQXPBeVVS1RUBsiG7PBuSENEhe0,4516
|
|
45
46
|
dao_ai/orchestration/__init__.py,sha256=i85CLfRR335NcCFhaXABcMkn6WZfXnJ8cHH4YZsZN0s,1622
|
|
46
47
|
dao_ai/orchestration/core.py,sha256=qoU7uMXBJCth-sqfu0jRE1L0GOn5H4LoZdRUY1Ib3DI,9585
|
|
47
48
|
dao_ai/orchestration/supervisor.py,sha256=alKMEEo9G5LhdpMvTVdAMel234cZj5_MguWl4wFB7XQ,9873
|
|
@@ -49,12 +50,12 @@ dao_ai/orchestration/swarm.py,sha256=8tp1eGmsQqqWpaDcjPoJckddPWohZdmmN0RGRJ_xzOA
|
|
|
49
50
|
dao_ai/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
50
51
|
dao_ai/providers/base.py,sha256=-fjKypCOk28h6vioPfMj9YZSw_3Kcbi2nMuAyY7vX9k,1383
|
|
51
52
|
dao_ai/providers/databricks.py,sha256=XxYkyoDYkwGV_Xg1IJBpGOl4d7U5HiFP4RtjjSLgenI,61437
|
|
52
|
-
dao_ai/tools/__init__.py,sha256=
|
|
53
|
+
dao_ai/tools/__init__.py,sha256=NfRpAKds_taHbx6gzLPWgtPXve-YpwzkoOAUflwxceM,1734
|
|
53
54
|
dao_ai/tools/agent.py,sha256=plIWALywRjaDSnot13nYehBsrHRpBUpsVZakoGeajOE,1858
|
|
54
55
|
dao_ai/tools/core.py,sha256=bRIN3BZhRQX8-Kpu3HPomliodyskCqjxynQmYbk6Vjs,3783
|
|
55
56
|
dao_ai/tools/email.py,sha256=A3TsCoQgJR7UUWR0g45OPRGDpVoYwctFs1MOZMTt_d4,7389
|
|
56
57
|
dao_ai/tools/genie.py,sha256=4e_5MeAe7kDzHbYeXuNPFbY5z8ci3ouj8l5254CZ2lA,8874
|
|
57
|
-
dao_ai/tools/mcp.py,sha256=
|
|
58
|
+
dao_ai/tools/mcp.py,sha256=0OfP4b4skcjeF2rzkOLYqd65ti1Mj55N_l8VoQlH9qo,17818
|
|
58
59
|
dao_ai/tools/memory.py,sha256=lwObKimAand22Nq3Y63tsv-AXQ5SXUigN9PqRjoWKes,1836
|
|
59
60
|
dao_ai/tools/python.py,sha256=jWFnZPni2sCdtd8D1CqXnZIPHnWkdK27bCJnBXpzhvo,1879
|
|
60
61
|
dao_ai/tools/search.py,sha256=cJ3D9FKr1GAR6xz55dLtRkjtQsI0WRueGt9TPDFpOxc,433
|
|
@@ -63,8 +64,8 @@ dao_ai/tools/sql.py,sha256=tKd1gjpLuKdQDyfmyYYtMiNRHDW6MGRbdEVaeqyB8Ok,7632
|
|
|
63
64
|
dao_ai/tools/time.py,sha256=tufJniwivq29y0LIffbgeBTIDE6VgrLpmVf8Qr90qjw,9224
|
|
64
65
|
dao_ai/tools/unity_catalog.py,sha256=AjQfW7bvV8NurqDLIyntYRv2eJuTwNdbvex1L5CRjOk,15534
|
|
65
66
|
dao_ai/tools/vector_search.py,sha256=oe2uBwl2TfeJIXPpwiS6Rmz7wcHczSxNyqS9P3hE6co,14542
|
|
66
|
-
dao_ai-0.1.
|
|
67
|
-
dao_ai-0.1.
|
|
68
|
-
dao_ai-0.1.
|
|
69
|
-
dao_ai-0.1.
|
|
70
|
-
dao_ai-0.1.
|
|
67
|
+
dao_ai-0.1.6.dist-info/METADATA,sha256=eux_1l0ANLlbQnRnq0IRgc8Q-ksY6sFmQa1_Vzd5_Kc,16685
|
|
68
|
+
dao_ai-0.1.6.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
69
|
+
dao_ai-0.1.6.dist-info/entry_points.txt,sha256=Xa-UFyc6gWGwMqMJOt06ZOog2vAfygV_DSwg1AiP46g,43
|
|
70
|
+
dao_ai-0.1.6.dist-info/licenses/LICENSE,sha256=YZt3W32LtPYruuvHE9lGk2bw6ZPMMJD8yLrjgHybyz4,1069
|
|
71
|
+
dao_ai-0.1.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|