tooluniverse 1.0.4__py3-none-any.whl → 1.0.5__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.
Potentially problematic release.
This version of tooluniverse might be problematic. Click here for more details.
- tooluniverse/__init__.py +17 -5
- tooluniverse/agentic_tool.py +8 -2
- tooluniverse/data/agentic_tools.json +2 -2
- tooluniverse/data/odphp_tools.json +354 -0
- tooluniverse/default_config.py +1 -0
- tooluniverse/llm_clients.py +201 -0
- tooluniverse/mcp_tool_registry.py +3 -3
- tooluniverse/odphp_tool.py +226 -0
- tooluniverse/remote/boltz/boltz_mcp_server.py +2 -2
- tooluniverse/remote/uspto_downloader/uspto_downloader_mcp_server.py +2 -2
- tooluniverse/smcp.py +204 -112
- tooluniverse/smcp_server.py +4 -7
- tooluniverse/test/test_claude_sdk.py +86 -0
- tooluniverse/test/test_odphp_tool.py +166 -0
- tooluniverse/test/test_openrouter_client.py +288 -0
- tooluniverse/test/test_stdio_hooks.py +1 -1
- tooluniverse/test/test_tool_finder.py +1 -1
- {tooluniverse-1.0.4.dist-info → tooluniverse-1.0.5.dist-info}/METADATA +100 -74
- {tooluniverse-1.0.4.dist-info → tooluniverse-1.0.5.dist-info}/RECORD +23 -18
- tooluniverse-1.0.5.dist-info/licenses/LICENSE +201 -0
- tooluniverse-1.0.4.dist-info/licenses/LICENSE +0 -21
- {tooluniverse-1.0.4.dist-info → tooluniverse-1.0.5.dist-info}/WHEEL +0 -0
- {tooluniverse-1.0.4.dist-info → tooluniverse-1.0.5.dist-info}/entry_points.txt +0 -0
- {tooluniverse-1.0.4.dist-info → tooluniverse-1.0.5.dist-info}/top_level.txt +0 -0
tooluniverse/smcp.py
CHANGED
|
@@ -1389,7 +1389,7 @@ class SMCP(FastMCP):
|
|
|
1389
1389
|
self.tool_finder_available = True
|
|
1390
1390
|
self.tool_finder_type = "Tool_Finder_LLM"
|
|
1391
1391
|
self.logger.info(
|
|
1392
|
-
"✅ Tool_Finder_LLM
|
|
1392
|
+
"✅ Tool_Finder_LLM available for advanced search"
|
|
1393
1393
|
)
|
|
1394
1394
|
return
|
|
1395
1395
|
|
|
@@ -1879,6 +1879,91 @@ class SMCP(FastMCP):
|
|
|
1879
1879
|
except Exception:
|
|
1880
1880
|
pass
|
|
1881
1881
|
|
|
1882
|
+
def _print_tooluniverse_banner(self):
|
|
1883
|
+
"""Print ToolUniverse branding banner after FastMCP banner with dynamic information."""
|
|
1884
|
+
# Get transport info if available
|
|
1885
|
+
transport_display = getattr(self, '_transport_type', 'Unknown')
|
|
1886
|
+
server_url = getattr(self, '_server_url', 'N/A')
|
|
1887
|
+
tools_count = len(self._exposed_tools)
|
|
1888
|
+
|
|
1889
|
+
# Map transport types to display names
|
|
1890
|
+
transport_map = {
|
|
1891
|
+
'stdio': 'STDIO',
|
|
1892
|
+
'streamable-http': 'Streamable-HTTP',
|
|
1893
|
+
'http': 'HTTP',
|
|
1894
|
+
'sse': 'SSE'
|
|
1895
|
+
}
|
|
1896
|
+
transport_name = transport_map.get(transport_display, transport_display)
|
|
1897
|
+
|
|
1898
|
+
# Format lines with proper alignment (matching FastMCP style)
|
|
1899
|
+
# Each line should be exactly 75 characters (emoji takes 2 display widths but counts as 1 in len())
|
|
1900
|
+
transport_line = f" 📦 Transport: {transport_name}"
|
|
1901
|
+
server_line = f" 🔗 Server URL: {server_url}"
|
|
1902
|
+
tools_line = f" 🧰 Loaded Tools: {tools_count}"
|
|
1903
|
+
|
|
1904
|
+
# Pad to exactly 75 characters (emoji counts as 1 in len() but displays as 2)
|
|
1905
|
+
transport_line = transport_line + " " * (75 - len(transport_line))
|
|
1906
|
+
server_line = server_line + " " * (75 - len(server_line))
|
|
1907
|
+
tools_line = tools_line + " " * (75 - len(tools_line))
|
|
1908
|
+
|
|
1909
|
+
banner = f"""
|
|
1910
|
+
╭────────────────────────────────────────────────────────────────────────────╮
|
|
1911
|
+
│ │
|
|
1912
|
+
│ 🧬 ToolUniverse SMCP Server 🧬 │
|
|
1913
|
+
│ │
|
|
1914
|
+
│ Bridging AI Agents with Scientific Computing Tools │
|
|
1915
|
+
│ │
|
|
1916
|
+
│{transport_line}│
|
|
1917
|
+
│{server_line}│
|
|
1918
|
+
│{tools_line}│
|
|
1919
|
+
│ │
|
|
1920
|
+
│ 🌐 Website: https://aiscientist.tools/ │
|
|
1921
|
+
│ 💻 GitHub: https://github.com/mims-harvard/ToolUniverse │
|
|
1922
|
+
│ │
|
|
1923
|
+
╰────────────────────────────────────────────────────────────────────────────╯
|
|
1924
|
+
"""
|
|
1925
|
+
print(banner)
|
|
1926
|
+
|
|
1927
|
+
def run(self, *args, **kwargs):
|
|
1928
|
+
"""
|
|
1929
|
+
Override run method to display ToolUniverse banner after FastMCP banner.
|
|
1930
|
+
|
|
1931
|
+
This method intercepts the parent's run() call to inject our custom banner
|
|
1932
|
+
immediately after FastMCP displays its startup banner.
|
|
1933
|
+
"""
|
|
1934
|
+
# Save transport information for banner display
|
|
1935
|
+
transport = kwargs.get('transport', args[0] if args else 'unknown')
|
|
1936
|
+
host = kwargs.get('host', '0.0.0.0')
|
|
1937
|
+
port = kwargs.get('port', 7000)
|
|
1938
|
+
|
|
1939
|
+
self._transport_type = transport
|
|
1940
|
+
|
|
1941
|
+
# Build server URL based on transport
|
|
1942
|
+
if transport == 'streamable-http' or transport == 'http':
|
|
1943
|
+
self._server_url = f"http://{host}:{port}/mcp"
|
|
1944
|
+
elif transport == 'sse':
|
|
1945
|
+
self._server_url = f"http://{host}:{port}"
|
|
1946
|
+
else:
|
|
1947
|
+
self._server_url = "N/A (stdio mode)"
|
|
1948
|
+
|
|
1949
|
+
# Use threading to print our banner shortly after FastMCP's banner
|
|
1950
|
+
import threading
|
|
1951
|
+
import time
|
|
1952
|
+
|
|
1953
|
+
def delayed_banner():
|
|
1954
|
+
"""Print ToolUniverse banner with a small delay to appear after FastMCP banner."""
|
|
1955
|
+
time.sleep(1.0) # Delay to ensure FastMCP banner displays first
|
|
1956
|
+
self._print_tooluniverse_banner()
|
|
1957
|
+
|
|
1958
|
+
# Start banner thread only on first run
|
|
1959
|
+
if not hasattr(self, '_tooluniverse_banner_shown'):
|
|
1960
|
+
self._tooluniverse_banner_shown = True
|
|
1961
|
+
banner_thread = threading.Thread(target=delayed_banner, daemon=True)
|
|
1962
|
+
banner_thread.start()
|
|
1963
|
+
|
|
1964
|
+
# Call parent's run method (blocking call)
|
|
1965
|
+
return super().run(*args, **kwargs)
|
|
1966
|
+
|
|
1882
1967
|
def run_simple(
|
|
1883
1968
|
self,
|
|
1884
1969
|
transport: Literal["stdio", "http", "sse"] = "http",
|
|
@@ -2085,120 +2170,127 @@ class SMCP(FastMCP):
|
|
|
2085
2170
|
func_params = []
|
|
2086
2171
|
param_annotations = {}
|
|
2087
2172
|
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2173
|
+
# Process parameters in two phases: required first, then optional
|
|
2174
|
+
# This ensures Python function signature validity (no default args before non-default)
|
|
2175
|
+
for is_required_phase in [True, False]:
|
|
2176
|
+
for param_name, param_info in properties.items():
|
|
2177
|
+
param_type = param_info.get("type", "string")
|
|
2178
|
+
param_description = param_info.get(
|
|
2179
|
+
"description", f"{param_name} parameter"
|
|
2180
|
+
)
|
|
2181
|
+
is_required = param_name in required_params
|
|
2182
|
+
|
|
2183
|
+
# Skip if not in current phase
|
|
2184
|
+
if is_required != is_required_phase:
|
|
2185
|
+
continue
|
|
2186
|
+
|
|
2187
|
+
# Map JSON schema types to Python types and create appropriate Field
|
|
2188
|
+
field_kwargs = {"description": param_description}
|
|
2189
|
+
|
|
2190
|
+
if param_type == "string":
|
|
2191
|
+
python_type = str
|
|
2192
|
+
# For string type, don't add json_schema_extra - let Pydantic handle it
|
|
2193
|
+
elif param_type == "integer":
|
|
2194
|
+
python_type = int
|
|
2195
|
+
# For integer type, don't add json_schema_extra - let Pydantic handle it
|
|
2196
|
+
elif param_type == "number":
|
|
2197
|
+
python_type = float
|
|
2198
|
+
# For number type, don't add json_schema_extra - let Pydantic handle it
|
|
2199
|
+
elif param_type == "boolean":
|
|
2200
|
+
python_type = bool
|
|
2201
|
+
# For boolean type, don't add json_schema_extra - let Pydantic handle it
|
|
2202
|
+
elif param_type == "array":
|
|
2203
|
+
python_type = list
|
|
2204
|
+
# Add array-specific schema information only for complex cases
|
|
2205
|
+
items_info = param_info.get("items", {})
|
|
2206
|
+
if items_info:
|
|
2207
|
+
# Clean up items definition - remove invalid fields
|
|
2208
|
+
cleaned_items = items_info.copy()
|
|
2209
|
+
|
|
2210
|
+
# Remove 'required' field from items (not valid in JSON Schema for array items)
|
|
2211
|
+
if "required" in cleaned_items:
|
|
2212
|
+
cleaned_items.pop("required")
|
|
2213
|
+
|
|
2214
|
+
field_kwargs["json_schema_extra"] = {
|
|
2215
|
+
"type": "array",
|
|
2216
|
+
"items": cleaned_items,
|
|
2217
|
+
}
|
|
2218
|
+
else:
|
|
2219
|
+
# If no items specified, default to string items
|
|
2220
|
+
field_kwargs["json_schema_extra"] = {
|
|
2221
|
+
"type": "array",
|
|
2222
|
+
"items": {"type": "string"},
|
|
2223
|
+
}
|
|
2224
|
+
elif param_type == "object":
|
|
2225
|
+
python_type = dict
|
|
2226
|
+
# Add object-specific schema information
|
|
2227
|
+
object_props = param_info.get("properties", {})
|
|
2228
|
+
if object_props:
|
|
2229
|
+
# Clean up the nested object properties - fix common schema issues
|
|
2230
|
+
cleaned_props = {}
|
|
2231
|
+
nested_required = []
|
|
2232
|
+
|
|
2233
|
+
for prop_name, prop_info in object_props.items():
|
|
2234
|
+
cleaned_prop = prop_info.copy()
|
|
2235
|
+
|
|
2236
|
+
# Fix string "True"/"False" in required field (common ToolUniverse issue)
|
|
2237
|
+
if "required" in cleaned_prop:
|
|
2238
|
+
req_value = cleaned_prop.pop("required")
|
|
2239
|
+
if req_value in ["True", "true", True]:
|
|
2240
|
+
nested_required.append(prop_name)
|
|
2241
|
+
# Remove the individual required field as it should be at object level
|
|
2242
|
+
|
|
2243
|
+
cleaned_props[prop_name] = cleaned_prop
|
|
2244
|
+
|
|
2245
|
+
# Create proper JSON schema for nested object
|
|
2246
|
+
object_schema = {"type": "object", "properties": cleaned_props}
|
|
2247
|
+
|
|
2248
|
+
# Add required array at object level if there are required fields
|
|
2249
|
+
if nested_required:
|
|
2250
|
+
object_schema["required"] = nested_required
|
|
2251
|
+
|
|
2252
|
+
field_kwargs["json_schema_extra"] = object_schema
|
|
2126
2253
|
else:
|
|
2127
|
-
#
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
"
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
# Create proper JSON schema for nested object
|
|
2154
|
-
object_schema = {"type": "object", "properties": cleaned_props}
|
|
2155
|
-
|
|
2156
|
-
# Add required array at object level if there are required fields
|
|
2157
|
-
if nested_required:
|
|
2158
|
-
object_schema["required"] = nested_required
|
|
2159
|
-
|
|
2160
|
-
field_kwargs["json_schema_extra"] = object_schema
|
|
2161
|
-
else:
|
|
2162
|
-
# For unknown types, default to string and only add type info if it's truly unknown
|
|
2163
|
-
python_type = str
|
|
2164
|
-
if param_type not in [
|
|
2165
|
-
"string",
|
|
2166
|
-
"integer",
|
|
2167
|
-
"number",
|
|
2168
|
-
"boolean",
|
|
2169
|
-
"array",
|
|
2170
|
-
"object",
|
|
2171
|
-
]:
|
|
2172
|
-
field_kwargs["json_schema_extra"] = {"type": param_type}
|
|
2173
|
-
|
|
2174
|
-
# Create Pydantic Field with enhanced schema information
|
|
2175
|
-
pydantic_field = Field(**field_kwargs)
|
|
2176
|
-
|
|
2177
|
-
if is_required:
|
|
2178
|
-
# Required parameter with description and schema info
|
|
2179
|
-
annotated_type = Annotated[python_type, pydantic_field]
|
|
2180
|
-
param_annotations[param_name] = annotated_type
|
|
2181
|
-
func_params.append(
|
|
2182
|
-
inspect.Parameter(
|
|
2183
|
-
param_name,
|
|
2184
|
-
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
2185
|
-
annotation=annotated_type,
|
|
2254
|
+
# For unknown types, default to string and only add type info if it's truly unknown
|
|
2255
|
+
python_type = str
|
|
2256
|
+
if param_type not in [
|
|
2257
|
+
"string",
|
|
2258
|
+
"integer",
|
|
2259
|
+
"number",
|
|
2260
|
+
"boolean",
|
|
2261
|
+
"array",
|
|
2262
|
+
"object",
|
|
2263
|
+
]:
|
|
2264
|
+
field_kwargs["json_schema_extra"] = {"type": param_type}
|
|
2265
|
+
|
|
2266
|
+
# Create Pydantic Field with enhanced schema information
|
|
2267
|
+
pydantic_field = Field(**field_kwargs)
|
|
2268
|
+
|
|
2269
|
+
if is_required:
|
|
2270
|
+
# Required parameter with description and schema info
|
|
2271
|
+
annotated_type = Annotated[python_type, pydantic_field]
|
|
2272
|
+
param_annotations[param_name] = annotated_type
|
|
2273
|
+
func_params.append(
|
|
2274
|
+
inspect.Parameter(
|
|
2275
|
+
param_name,
|
|
2276
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
2277
|
+
annotation=annotated_type,
|
|
2278
|
+
)
|
|
2186
2279
|
)
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2280
|
+
else:
|
|
2281
|
+
# Optional parameter with description, schema info and default value
|
|
2282
|
+
annotated_type = Annotated[
|
|
2283
|
+
Union[python_type, type(None)], pydantic_field
|
|
2284
|
+
]
|
|
2285
|
+
param_annotations[param_name] = annotated_type
|
|
2286
|
+
func_params.append(
|
|
2287
|
+
inspect.Parameter(
|
|
2288
|
+
param_name,
|
|
2289
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
2290
|
+
default=None,
|
|
2291
|
+
annotation=annotated_type,
|
|
2292
|
+
)
|
|
2200
2293
|
)
|
|
2201
|
-
)
|
|
2202
2294
|
|
|
2203
2295
|
# Create the async function with dynamic signature
|
|
2204
2296
|
if not properties:
|
tooluniverse/smcp_server.py
CHANGED
|
@@ -111,7 +111,6 @@ Examples:
|
|
|
111
111
|
auto_expose_tools=True,
|
|
112
112
|
search_enabled=True,
|
|
113
113
|
max_workers=5,
|
|
114
|
-
stateless_http=True, # Enable stateless mode for MCPAutoLoaderTool compatibility
|
|
115
114
|
hooks_enabled=hooks_enabled,
|
|
116
115
|
hook_config=hook_config,
|
|
117
116
|
hook_type=args.hook_type,
|
|
@@ -254,8 +253,8 @@ Examples:
|
|
|
254
253
|
# Server configuration (stdio-specific)
|
|
255
254
|
parser.add_argument(
|
|
256
255
|
"--name",
|
|
257
|
-
default="SMCP
|
|
258
|
-
help="Server name (default: SMCP
|
|
256
|
+
default="ToolUniverse SMCP Server",
|
|
257
|
+
help="Server name (default: ToolUniverse SMCP Server)",
|
|
259
258
|
)
|
|
260
259
|
parser.add_argument(
|
|
261
260
|
"--no-search",
|
|
@@ -528,7 +527,6 @@ Examples:
|
|
|
528
527
|
exclude_tool_types=exclude_tool_types,
|
|
529
528
|
search_enabled=not args.no_search,
|
|
530
529
|
max_workers=args.max_workers,
|
|
531
|
-
stateless_http=True, # Enable stateless mode for MCPAutoLoaderTool compatibility
|
|
532
530
|
hooks_enabled=hooks_enabled,
|
|
533
531
|
hook_config=hook_config,
|
|
534
532
|
hook_type=hook_type,
|
|
@@ -685,8 +683,8 @@ Examples:
|
|
|
685
683
|
)
|
|
686
684
|
parser.add_argument(
|
|
687
685
|
"--name",
|
|
688
|
-
default="SMCP
|
|
689
|
-
help="Server name (default: SMCP
|
|
686
|
+
default="ToolUniverse SMCP Server",
|
|
687
|
+
help="Server name (default: ToolUniverse SMCP Server)",
|
|
690
688
|
)
|
|
691
689
|
parser.add_argument(
|
|
692
690
|
"--no-search",
|
|
@@ -956,7 +954,6 @@ Examples:
|
|
|
956
954
|
exclude_tool_types=exclude_tool_types,
|
|
957
955
|
search_enabled=not args.no_search,
|
|
958
956
|
max_workers=args.max_workers,
|
|
959
|
-
stateless_http=True, # Enable stateless mode for MCPAutoLoaderTool compatibility
|
|
960
957
|
hooks_enabled=hooks_enabled,
|
|
961
958
|
hook_config=hook_config,
|
|
962
959
|
hook_type=args.hook_type,
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
from claude_agent_sdk import ClaudeAgentOptions, query
|
|
4
|
+
from dotenv import load_dotenv
|
|
5
|
+
|
|
6
|
+
load_dotenv()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def delegate_task(
|
|
10
|
+
prompt: str,
|
|
11
|
+
append_system_prompt: str,
|
|
12
|
+
):
|
|
13
|
+
"""Delegate a task to an expert
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
prompt: The prompt describing the task to delegate
|
|
17
|
+
append_system_prompt: The system prompt describing the expert
|
|
18
|
+
Returns:
|
|
19
|
+
The result of the delegation
|
|
20
|
+
"""
|
|
21
|
+
import os
|
|
22
|
+
cwd = os.getcwd()
|
|
23
|
+
|
|
24
|
+
async for message in query(
|
|
25
|
+
prompt=prompt,
|
|
26
|
+
options=ClaudeAgentOptions(
|
|
27
|
+
system_prompt={
|
|
28
|
+
"type": "preset",
|
|
29
|
+
"preset": "claude_code",
|
|
30
|
+
"append": append_system_prompt,
|
|
31
|
+
}, # Use the preset
|
|
32
|
+
cwd=cwd,
|
|
33
|
+
permission_mode="bypassPermissions",
|
|
34
|
+
mcp_servers={
|
|
35
|
+
"tooluniverse": {
|
|
36
|
+
"type": "stdio",
|
|
37
|
+
"command": "tooluniverse-smcp-stdio",
|
|
38
|
+
"args": [],
|
|
39
|
+
"env": {},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
),
|
|
43
|
+
):
|
|
44
|
+
# Print all message types to see tool usage
|
|
45
|
+
message_type = type(message).__name__
|
|
46
|
+
print(f"\n--- Message Type: {message_type} ---")
|
|
47
|
+
|
|
48
|
+
# Check for tool use messages
|
|
49
|
+
if message_type == "ToolUseMessage":
|
|
50
|
+
print(f"Tool Name: {message.name}")
|
|
51
|
+
print(f"Tool Input: {message.input}")
|
|
52
|
+
elif message_type == "ToolResultMessage":
|
|
53
|
+
print(f"Tool: {message.tool_use_id}")
|
|
54
|
+
result_preview = (
|
|
55
|
+
str(message.content)[:200]
|
|
56
|
+
if hasattr(message, "content")
|
|
57
|
+
else str(message)[:200]
|
|
58
|
+
)
|
|
59
|
+
print(f"Result: {result_preview}...")
|
|
60
|
+
elif message_type == "TextMessage":
|
|
61
|
+
text_preview = (
|
|
62
|
+
str(message.text)[:200]
|
|
63
|
+
if hasattr(message, "text")
|
|
64
|
+
else str(message)[:200]
|
|
65
|
+
)
|
|
66
|
+
print(f"Text: {text_preview}...")
|
|
67
|
+
elif message_type == "ResultMessage":
|
|
68
|
+
return {
|
|
69
|
+
"status": "success",
|
|
70
|
+
"result": message.result,
|
|
71
|
+
}
|
|
72
|
+
else:
|
|
73
|
+
# Print any other message types for debugging
|
|
74
|
+
print(f"Message: {message}")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
# result = asyncio.run(conduct_research("What is the capital of France?"))
|
|
79
|
+
# print(result)
|
|
80
|
+
result = asyncio.run(
|
|
81
|
+
delegate_task(
|
|
82
|
+
"What tools do you have available? Return a list of all available tool names",
|
|
83
|
+
"You are a helpful assistant",
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
print(result)
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from tooluniverse import ToolUniverse
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
schema_path = os.path.join(os.path.dirname(__file__), "..", "data", "odphp_tools.json")
|
|
7
|
+
with open(schema_path) as f:
|
|
8
|
+
schemas = {tool["name"]: tool["return_schema"] for tool in json.load(f)}
|
|
9
|
+
|
|
10
|
+
tooluni = ToolUniverse()
|
|
11
|
+
tooluni.load_tools()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def summarize_result(tool_name, res):
|
|
15
|
+
if isinstance(res, str):
|
|
16
|
+
return f"{tool_name}: INVALID Raw string response: {res[:200]}..."
|
|
17
|
+
if isinstance(res, dict):
|
|
18
|
+
if "error" in res:
|
|
19
|
+
return f"{tool_name}: ERROR {res['error']}"
|
|
20
|
+
data = res.get("data", {})
|
|
21
|
+
total = data.get("Total") if isinstance(data, dict) else None
|
|
22
|
+
msg = f"{tool_name}: SUCCESS"
|
|
23
|
+
if isinstance(total, int):
|
|
24
|
+
msg += f" | Total={total}"
|
|
25
|
+
|
|
26
|
+
if tool_name == "odphp_myhealthfinder":
|
|
27
|
+
heading = data.get("MyHFHeading", "")
|
|
28
|
+
resources = (data.get("Resources", {}).get("All", {}).get("Resource", [])) or []
|
|
29
|
+
first_title = resources[0].get("Title") if resources else None
|
|
30
|
+
msg += f" | Heading='{heading[:60]}...'"
|
|
31
|
+
if first_title:
|
|
32
|
+
msg += f" | FirstResource='{first_title}'"
|
|
33
|
+
callouts = (data.get("Callouts", {}).get("All", {}).get("Resource", [])) or []
|
|
34
|
+
if callouts and callouts[0].get("MyHFTitle"):
|
|
35
|
+
msg += f" | FirstCallout='{callouts[0].get('MyHFTitle')}'"
|
|
36
|
+
|
|
37
|
+
elif tool_name == "odphp_itemlist":
|
|
38
|
+
items = data.get("Items", {}).get("Item", []) or []
|
|
39
|
+
titles = [i.get("Title") for i in items[:3]]
|
|
40
|
+
if titles:
|
|
41
|
+
msg += f" | ExampleItems={titles}"
|
|
42
|
+
|
|
43
|
+
elif tool_name == "odphp_topicsearch":
|
|
44
|
+
resources = data.get("Resources", {}).get("Resource", []) or []
|
|
45
|
+
titles = [r.get("Title") for r in resources[:3]]
|
|
46
|
+
if titles:
|
|
47
|
+
msg += f" | ExampleTopics={titles}"
|
|
48
|
+
|
|
49
|
+
elif tool_name.startswith("odphp_outlink_fetch"):
|
|
50
|
+
results = res.get("results") or []
|
|
51
|
+
if results:
|
|
52
|
+
first = results[0]
|
|
53
|
+
msg += f" | url={first.get('url')} status={first.get('status')} type={first.get('content_type')}"
|
|
54
|
+
if first.get("title"):
|
|
55
|
+
msg += f" | Title='{first['title'][:50]}...'"
|
|
56
|
+
if first.get("text"):
|
|
57
|
+
snippet = first["text"][:80].replace("\n", " ")
|
|
58
|
+
msg += f" | TextSnippet='{snippet}...'"
|
|
59
|
+
|
|
60
|
+
expected_keys = schemas.get(tool_name, {}).get("properties", {}).keys()
|
|
61
|
+
missing = [k for k in expected_keys if k not in data and k not in res]
|
|
62
|
+
if missing:
|
|
63
|
+
msg += f" | WARNING: Missing keys {missing}"
|
|
64
|
+
else:
|
|
65
|
+
msg += " | Schema OK"
|
|
66
|
+
return msg
|
|
67
|
+
return f"{tool_name}: INVALID Unexpected type {type(res)}"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_01_myhealthfinder_valid():
|
|
71
|
+
res = tooluni.run({"name": "odphp_myhealthfinder",
|
|
72
|
+
"arguments": {"age": 35, "sex": "female", "pregnant": "no", "lang": "en"}})
|
|
73
|
+
print(summarize_result("odphp_myhealthfinder", res))
|
|
74
|
+
assert isinstance(res, dict) and not res.get("error")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_02_itemlist_valid():
|
|
78
|
+
res = tooluni.run({"name": "odphp_itemlist", "arguments": {"type": "topic", "lang": "en"}})
|
|
79
|
+
print(summarize_result("odphp_itemlist", res))
|
|
80
|
+
assert isinstance(res, dict) and not res.get("error")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_03_topicsearch_keyword_valid():
|
|
84
|
+
res = tooluni.run({"name": "odphp_topicsearch", "arguments": {"keyword": "cancer", "lang": "en"}})
|
|
85
|
+
print(summarize_result("odphp_topicsearch", res))
|
|
86
|
+
assert isinstance(res, dict) and not res.get("error")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_04_invalid_types_fail_fast():
|
|
90
|
+
r1 = tooluni.run({"name": "odphp_myhealthfinder", "arguments": {"age": "banana"}})
|
|
91
|
+
r2 = tooluni.run({"name": "odphp_topicsearch", "arguments": {"topicId": 123}})
|
|
92
|
+
print("Expected type errors:", r1, r2)
|
|
93
|
+
assert isinstance(r1, str) and "Type mismatches" in r1
|
|
94
|
+
assert isinstance(r2, str) and "Type mismatches" in r2
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_05_sections_case_and_strip_html():
|
|
98
|
+
res = tooluni.run({"name": "odphp_topicsearch",
|
|
99
|
+
"arguments": {"keyword": "Keep Your Heart Healthy", "lang": "en", "strip_html": True}})
|
|
100
|
+
print(summarize_result("odphp_topicsearch", res))
|
|
101
|
+
assert isinstance(res, dict) and not res.get("error")
|
|
102
|
+
data = res.get("data") or {}
|
|
103
|
+
resources = (data.get("Resources") or {}).get("Resource") or []
|
|
104
|
+
if resources:
|
|
105
|
+
s_any = resources[0].get("Sections", {})
|
|
106
|
+
arr = s_any.get("Section") or s_any.get("section") or []
|
|
107
|
+
assert isinstance(arr, list)
|
|
108
|
+
assert "PlainSections" in resources[0]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_06_outlink_fetch_accessible_version():
|
|
112
|
+
url = "https://odphp.health.gov/myhealthfinder/health-conditions/heart-health/keep-your-heart-healthy"
|
|
113
|
+
res = tooluni.run({"name": "odphp_outlink_fetch",
|
|
114
|
+
"arguments": {"urls": [url], "max_chars": 4000}})
|
|
115
|
+
print(summarize_result("odphp_outlink_fetch", res))
|
|
116
|
+
assert isinstance(res, dict) and not res.get("error")
|
|
117
|
+
results = res.get("results") or []
|
|
118
|
+
assert results and results[0].get("status") in (200, 301, 302)
|
|
119
|
+
if "text/html" in (results[0].get("content_type") or ""):
|
|
120
|
+
assert len(results[0].get("text", "")) > 100
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_07_itemlist_spanish():
|
|
124
|
+
res = tooluni.run({"name": "odphp_itemlist", "arguments": {"type": "topic", "lang": "es"}})
|
|
125
|
+
print(summarize_result("odphp_itemlist", res))
|
|
126
|
+
assert isinstance(res, dict) and not res.get("error")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_08_topicsearch_by_category():
|
|
130
|
+
cats = tooluni.run({"name": "odphp_itemlist", "arguments": {"type": "category", "lang": "en"}})
|
|
131
|
+
first_cat = (cats.get("data", {}).get("Items", {}).get("Item") or [])[0]
|
|
132
|
+
cid = first_cat.get("Id")
|
|
133
|
+
res = tooluni.run({"name": "odphp_topicsearch", "arguments": {"categoryId": cid, "lang": "en"}})
|
|
134
|
+
print(summarize_result("odphp_topicsearch", res))
|
|
135
|
+
assert isinstance(res, dict) and not res.get("error")
|
|
136
|
+
|
|
137
|
+
def test_09_outlink_fetch_pdf():
|
|
138
|
+
url = "https://odphp.health.gov/sites/default/files/2021-12/DGA_Pregnancy_FactSheet-508.pdf"
|
|
139
|
+
res = tooluni.run({"name": "odphp_outlink_fetch",
|
|
140
|
+
"arguments": {"urls": [url], "max_chars": 1000}})
|
|
141
|
+
print(summarize_result("odphp_outlink_fetch_pdf", res))
|
|
142
|
+
|
|
143
|
+
assert isinstance(res, dict) and not res.get("error")
|
|
144
|
+
results = res.get("results") or []
|
|
145
|
+
assert results, "No results returned for PDF URL"
|
|
146
|
+
|
|
147
|
+
ctype = results[0].get("content_type", "")
|
|
148
|
+
assert ctype.startswith("application/pdf"), f"Expected PDF but got {ctype}"
|
|
149
|
+
|
|
150
|
+
# Ensure text extraction worked at least partially
|
|
151
|
+
text = results[0].get("text", "")
|
|
152
|
+
assert text and len(text) > 50, "Extracted PDF text too short"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
if __name__ == "__main__":
|
|
156
|
+
print("\nRunning ODPHP tool tests...\n")
|
|
157
|
+
test_01_myhealthfinder_valid()
|
|
158
|
+
test_02_itemlist_valid()
|
|
159
|
+
test_03_topicsearch_keyword_valid()
|
|
160
|
+
test_04_invalid_types_fail_fast()
|
|
161
|
+
test_05_sections_case_and_strip_html()
|
|
162
|
+
test_06_outlink_fetch_accessible_version()
|
|
163
|
+
test_07_itemlist_spanish()
|
|
164
|
+
test_08_topicsearch_by_category()
|
|
165
|
+
test_09_outlink_fetch_pdf()
|
|
166
|
+
print("\nAll ODPHP tests executed.\n")
|