Skip to content

Commit b147703

Browse files
committed
improved stdio and logging
1 parent 776eb24 commit b147703

File tree

7 files changed

+838
-733
lines changed

7 files changed

+838
-733
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "chuk-tool-processor"
7-
version = "0.6.24"
7+
version = "0.6.25"
88
description = "Async-native framework for registering, discovering, and executing tools referenced in LLM responses"
99
readme = "README.md"
1010
requires-python = ">=3.11"

src/chuk_tool_processor/core/processor.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -250,11 +250,13 @@ async def process(
250250

251251
# Execute tool calls
252252
async with log_context_span("tool_execution", {"num_calls": len(calls)}):
253-
# Check if any tools are unknown
253+
# Check if any tools are unknown - search across all namespaces
254254
unknown_tools = []
255+
all_tools = await self.registry.list_tools() # Returns list of (namespace, name) tuples
256+
tool_names_in_registry = {name for ns, name in all_tools}
257+
255258
for call in calls:
256-
tool = await self.registry.get_tool(call.tool)
257-
if not tool:
259+
if call.tool not in tool_names_in_registry:
258260
unknown_tools.append(call.tool)
259261

260262
if unknown_tools:

src/chuk_tool_processor/logging/metrics.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ async def log_tool_execution(
4545
cached: Whether the result was retrieved from cache
4646
attempts: Number of execution attempts
4747
"""
48-
self.logger.info(
48+
self.logger.debug(
4949
f"Tool execution metric: {tool}",
5050
extra={
5151
"context": {
@@ -76,7 +76,7 @@ async def log_parser_metric(
7676
duration: Parsing duration in seconds
7777
num_calls: Number of tool calls parsed
7878
"""
79-
self.logger.info(
79+
self.logger.debug(
8080
f"Parser metric: {parser}",
8181
extra={
8282
"context": {

src/chuk_tool_processor/mcp/transport/stdio_transport.py

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -389,9 +389,40 @@ async def get_tools(self) -> list[dict[str, Any]]:
389389
try:
390390
response = await asyncio.wait_for(send_tools_list(*self._streams), timeout=self.default_timeout)
391391

392-
# Normalize response
393-
if isinstance(response, dict):
394-
tools = response.get("tools", [])
392+
# Normalize response - handle multiple formats including Pydantic models
393+
# 1. Check if it's a Pydantic model with tools attribute (e.g., ListToolsResult from chuk_mcp)
394+
if hasattr(response, "tools"):
395+
tools = response.tools
396+
# Convert Pydantic Tool models to dicts if needed
397+
if tools and len(tools) > 0 and hasattr(tools[0], "model_dump"):
398+
tools = [tool.model_dump() if hasattr(tool, "model_dump") else tool for tool in tools]
399+
elif tools and len(tools) > 0 and hasattr(tools[0], "dict"):
400+
# Older Pydantic versions use dict() instead of model_dump()
401+
tools = [tool.dict() if hasattr(tool, "dict") else tool for tool in tools]
402+
# 2. Check if it's a Pydantic model that can be dumped
403+
elif hasattr(response, "model_dump"):
404+
dumped = response.model_dump()
405+
tools = dumped.get("tools", [])
406+
# 3. Handle dict responses
407+
elif isinstance(response, dict):
408+
# Check for tools at top level
409+
if "tools" in response:
410+
tools = response["tools"]
411+
# Check for nested result.tools (common in some MCP implementations)
412+
elif "result" in response and isinstance(response["result"], dict):
413+
tools = response["result"].get("tools", [])
414+
# Check if response itself is the result with MCP structure
415+
elif "jsonrpc" in response and "result" in response:
416+
result = response["result"]
417+
if isinstance(result, dict):
418+
tools = result.get("tools", [])
419+
elif isinstance(result, list):
420+
tools = result
421+
else:
422+
tools = []
423+
else:
424+
tools = []
425+
# 4. Handle list responses
395426
elif isinstance(response, list):
396427
tools = response
397428
else:
@@ -443,7 +474,7 @@ async def call_tool(
443474
return {"isError": True, "error": "Failed to recover connection"}
444475

445476
response = await asyncio.wait_for(
446-
send_tools_call(*self._streams, tool_name, arguments), timeout=tool_timeout
477+
send_tools_call(*self._streams, tool_name, arguments, timeout=tool_timeout), timeout=tool_timeout
447478
)
448479

449480
response_time = time.time() - start_time

tests/core/test_processor.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ def mock_registry():
2121
"""Create a mock registry."""
2222
registry = Mock()
2323
registry.get_tool = AsyncMock(return_value=Mock())
24+
registry.list_tools = AsyncMock(return_value=[])
2425
return registry
2526

2627

tests/logging/test_metrics.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ async def test_log_tool_execution():
4040
)
4141

4242
# Check context
43-
mock_logger.info.assert_called_once()
44-
args, kwargs = mock_logger.info.call_args
43+
mock_logger.debug.assert_called_once()
44+
args, kwargs = mock_logger.debug.call_args
4545
ctx = kwargs["extra"]["context"]
4646
assert ctx["success"] is False
4747
assert ctx["error"] == "Test error message"
@@ -65,8 +65,8 @@ async def test_global_metrics_instance():
6565
await metrics.log_tool_execution(tool="global_test", success=True, duration=1.0)
6666

6767
# Check logging
68-
mock_logger.info.assert_called_once()
69-
args, kwargs = mock_logger.info.call_args
68+
mock_logger.debug.assert_called_once()
69+
args, kwargs = mock_logger.debug.call_args
7070
assert "global_test" in args[0]
7171
assert kwargs["extra"]["context"]["tool"] == "global_test"
7272
finally:
@@ -101,25 +101,25 @@ async def log_task(i):
101101
await asyncio.gather(*tasks)
102102

103103
# Should have logged 5 times
104-
assert mock_logger.info.call_count == 5
104+
assert mock_logger.debug.call_count == 5
105105

106106
# Reset the mock before the additional call
107-
mock_logger.info.reset_mock()
107+
mock_logger.debug.reset_mock()
108108

109109
# Now make a separate call with a clean mock state
110110
await logger.log_tool_execution(
111111
tool="test_tool", success=True, duration=0.123, error=None, cached=True, attempts=2
112112
)
113113

114114
# Check that this specific call was logged once
115-
mock_logger.info.assert_called_once()
116-
args, kwargs = mock_logger.info.call_args
115+
mock_logger.debug.assert_called_once()
116+
args, kwargs = mock_logger.debug.call_args
117117
assert "test_tool" in args[0]
118118
assert kwargs["extra"]["context"]["tool"] == "test_tool"
119119

120120
# Check that all tools were logged across all calls
121121
# Reset the mock again for clean state
122-
mock_logger.info.reset_mock()
122+
mock_logger.debug.reset_mock()
123123

124124
# Log each tool again to get a fresh set of calls
125125
for i in range(5):
@@ -129,7 +129,7 @@ async def log_task(i):
129129

130130
# Now check the tool names
131131
tool_names = set()
132-
for call in mock_logger.info.call_args_list:
132+
for call in mock_logger.debug.call_args_list:
133133
args, kwargs = call
134134
tool_name = kwargs["extra"]["context"]["tool"]
135135
tool_names.add(tool_name)
@@ -153,8 +153,8 @@ async def test_log_parser_metric():
153153
await logger.log_parser_metric(parser="xml_parser", success=True, duration=0.456, num_calls=5)
154154

155155
# Check logging
156-
mock_logger.info.assert_called_once()
157-
args, kwargs = mock_logger.info.call_args
156+
mock_logger.debug.assert_called_once()
157+
args, kwargs = mock_logger.debug.call_args
158158
assert "xml_parser" in args[0]
159159
assert "context" in kwargs["extra"]
160160
ctx = kwargs["extra"]["context"]

0 commit comments

Comments
 (0)