Skip to content

Commit b7c2ffc

Browse files
authored
Merge pull request #25 from chughtapan/bfcl_setup
Update tool calling setup to better align with BFCL experiments + update submodules
2 parents d109daf + 50bb99e commit b7c2ffc

2 files changed

Lines changed: 85 additions & 44 deletions

File tree

tests/benchmarks/bfcl/data

Submodule data updated 43 files

tests/benchmarks/bfcl/mcp_server.py

Lines changed: 84 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -12,78 +12,119 @@
1212
import sys
1313
from typing import Any
1414

15+
from bfcl_eval.constants.eval_config import MULTI_TURN_FUNC_DOC_PATH
1516
from bfcl_eval.constants.executable_backend_config import (
1617
CLASS_FILE_PATH_MAPPING,
18+
MULTI_TURN_FUNC_DOC_FILE_MAPPING,
1719
STATELESS_CLASSES,
1820
)
1921
from mcp.server.fastmcp import FastMCP
2022

2123

22-
def load_api_class(target_class_name: str) -> Any:
23-
"""Load the specified API class dynamically and return instance."""
24-
if target_class_name not in CLASS_FILE_PATH_MAPPING:
25-
raise ValueError(f"Unknown class: {target_class_name}")
24+
def load_api_class(class_name: str) -> Any:
25+
"""Load and instantiate the specified API class."""
26+
module = importlib.import_module(CLASS_FILE_PATH_MAPPING[class_name])
27+
return getattr(module, class_name)()
2628

27-
# Load the class
28-
module = importlib.import_module(CLASS_FILE_PATH_MAPPING[target_class_name])
29-
instance = getattr(module, target_class_name)()
30-
return instance
3129

30+
def load_func_docs(class_name: str) -> dict[str, dict[str, Any]]:
31+
"""Load BFCL's function documentation for a class.
3232
33-
def load_scenario_from_test(test_file: str, test_id: str, target_class_name: str) -> dict[str, Any]:
34-
"""Load scenario configuration from test file."""
35-
scenario = {}
36-
if test_file and test_id:
37-
try:
38-
with open(test_file) as f:
39-
for line in f:
40-
if line.strip():
41-
entry = json.loads(line)
42-
if entry.get("id") == test_id:
43-
if "initial_config" in entry and target_class_name in entry["initial_config"]:
44-
scenario = entry["initial_config"][target_class_name]
45-
break
46-
except Exception as e:
47-
print(f"Warning: Could not load scenario: {e}", file=sys.stderr)
48-
return scenario
33+
Returns a dict mapping function names to their full documentation,
34+
including rich descriptions and parameter schemas.
35+
"""
36+
if class_name not in MULTI_TURN_FUNC_DOC_FILE_MAPPING:
37+
return {}
38+
39+
doc_path = MULTI_TURN_FUNC_DOC_PATH / MULTI_TURN_FUNC_DOC_FILE_MAPPING[class_name]
40+
if not doc_path.exists():
41+
return {}
42+
43+
docs = {}
44+
with open(doc_path) as f:
45+
for line in f:
46+
if line.strip():
47+
doc = json.loads(line)
48+
docs[doc["name"]] = doc
49+
return docs
50+
51+
52+
def load_scenario_from_test(test_file: str, test_id: str, class_name: str) -> dict[str, Any]:
53+
"""Load initial scenario configuration from a test file."""
54+
if not test_file or not test_id:
55+
return {}
56+
57+
with open(test_file) as f:
58+
for line in f:
59+
if line.strip():
60+
entry = json.loads(line)
61+
if entry.get("id") == test_id:
62+
config: dict[str, Any] = entry.get("initial_config", {}).get(class_name, {})
63+
return config
64+
65+
return {}
66+
67+
68+
def patch_tool_with_func_doc(server: FastMCP, func_docs: dict[str, dict[str, Any]]) -> None:
69+
"""Patch registered tools with BFCL's richer function documentation.
70+
71+
FastMCP's introspection doesn't extract parameter descriptions from docstrings.
72+
BFCL provides pre-compiled JSON docs with proper descriptions, so we overlay them.
73+
"""
74+
for tool_name, tool in server._tool_manager._tools.items():
75+
if tool_name not in func_docs:
76+
continue
77+
78+
doc = func_docs[tool_name]
79+
80+
# Patch tool description
81+
tool.description = doc.get("description", tool.description)
82+
83+
# Patch parameter descriptions
84+
doc_params = doc.get("parameters", {}).get("properties", {})
85+
tool_params = tool.parameters.get("properties", {})
86+
87+
for param_name, param_doc in doc_params.items():
88+
if param_name in tool_params and "description" in param_doc:
89+
tool_params[param_name]["description"] = param_doc["description"]
4990

5091

5192
async def main() -> None:
5293
parser = argparse.ArgumentParser(description="MCP Server for BFCL API classes")
5394
parser.add_argument("class_name", help="API class name to load")
5495
parser.add_argument("test_file", nargs="?", help="Test file path (optional)")
5596
parser.add_argument("test_id", nargs="?", help="Test ID (optional)")
56-
5797
args = parser.parse_args()
5898

59-
target_class_name = args.class_name
99+
class_name = args.class_name
60100

61-
if target_class_name not in CLASS_FILE_PATH_MAPPING:
62-
print("Usage: python api_server.py <ClassName> [test_file.json test_id]", file=sys.stderr)
101+
if class_name not in CLASS_FILE_PATH_MAPPING:
102+
print("Usage: python mcp_server.py <ClassName> [test_file.json test_id]", file=sys.stderr)
63103
print(f"Available classes: {', '.join(CLASS_FILE_PATH_MAPPING.keys())}", file=sys.stderr)
64104
sys.exit(1)
65105

66-
try:
67-
api_instance = load_api_class(target_class_name)
68-
print(f"Successfully loaded {target_class_name}", file=sys.stderr)
106+
# Load the API class
107+
api = load_api_class(class_name)
108+
print(f"Loaded {class_name}", file=sys.stderr)
69109

70-
# Load scenario if needed
71-
if hasattr(api_instance, "_load_scenario") and target_class_name not in STATELESS_CLASSES:
72-
scenario = load_scenario_from_test(args.test_file, args.test_id, target_class_name)
73-
api_instance._load_scenario(scenario)
74-
except Exception as e:
75-
print(f"Error loading {target_class_name}: {e}", file=sys.stderr)
76-
sys.exit(1)
110+
# Initialize scenario state if needed
111+
if hasattr(api, "_load_scenario") and class_name not in STATELESS_CLASSES:
112+
scenario = load_scenario_from_test(args.test_file, args.test_id, class_name)
113+
api._load_scenario(scenario)
77114

78-
# Create FastMCP server
79-
server = FastMCP(f"{target_class_name.lower()}-api")
115+
# Load BFCL's function documentation
116+
func_docs = load_func_docs(class_name)
80117

81-
# Register all API methods as tools
82-
for method_name, method in inspect.getmembers(api_instance, predicate=inspect.ismethod):
118+
# Create server and register tools
119+
server = FastMCP(f"{class_name.lower()}-api")
120+
121+
for method_name, method in inspect.getmembers(api, predicate=inspect.ismethod):
83122
if not method_name.startswith("_"):
84123
server.add_tool(method, name=method_name)
85124

86-
# Run the server
125+
# Patch tools with BFCL's richer descriptions
126+
patch_tool_with_func_doc(server, func_docs)
127+
87128
await server.run_stdio_async()
88129

89130

0 commit comments

Comments
 (0)