Skip to content

Commit 8031d10

Browse files
committed
fix: Address PR review feedback
- Add warning logging when resolve_operation falls back to full tool name - Document that resolve_operation is for tracing/status, not routing - Add proper types to Tool class (Callable, ToolDefinition) with ConfigDict - Remove unnecessary pass statement in prometheus provider - Clarify _invoke closure default arg captures method to avoid late binding - Add explanatory comments for AttributeError handlers probing response formats - Simplify getattr comment in tools() method
1 parent 0684dbc commit 8031d10

File tree

5 files changed

+40
-23
lines changed

5 files changed

+40
-23
lines changed

redis_sre_agent/agent/langgraph_agent.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1855,19 +1855,22 @@ async def _retry_with_backoff(
18551855
try:
18561856
return await func()
18571857
except Exception as e:
1858-
# Enrich logging with OpenAI/HTTP error details when available
1858+
# Enrich logging with OpenAI/HTTP error details when available.
1859+
# Different exception types expose status/body via different attributes,
1860+
# so we probe multiple patterns and ignore AttributeError when not found.
18591861
try:
18601862
status = None
18611863
body = None
18621864
try:
18631865
status = e.status_code # type: ignore[attr-defined]
18641866
except AttributeError:
1867+
# Try .response.status_code pattern (e.g., httpx exceptions)
18651868
try:
18661869
resp = e.response # type: ignore[attr-defined]
18671870
if resp:
18681871
status = resp.status_code
18691872
except AttributeError:
1870-
pass
1873+
pass # No status code available
18711874
try:
18721875
raw_body = e.body # type: ignore[attr-defined]
18731876
if isinstance(raw_body, (str, bytes)):
@@ -1877,6 +1880,7 @@ async def _retry_with_backoff(
18771880
else raw_body
18781881
)
18791882
except AttributeError:
1883+
# Try .response.text or .response.content pattern
18801884
try:
18811885
resp = e.response # type: ignore[attr-defined]
18821886
if resp is not None:
@@ -1886,11 +1890,11 @@ async def _retry_with_backoff(
18861890
try:
18871891
body = resp.content
18881892
except AttributeError:
1889-
pass
1893+
pass # No body available
18901894
if isinstance(body, (bytes, bytearray)):
18911895
body = body.decode("utf-8", errors="ignore")
18921896
except AttributeError:
1893-
pass
1897+
pass # No response object available
18941898
if status or body:
18951899
snippet = (body or "")[:2000]
18961900
logger.error(

redis_sre_agent/observability/llm_metrics.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,19 @@
4242

4343

4444
def _get_model_name(llm: Any) -> str:
45+
"""Extract model name from LLM object, trying common attribute patterns."""
4546
try:
4647
model = llm.model # type: ignore[attr-defined]
4748
if model:
4849
return model
4950
except AttributeError:
50-
pass
51+
pass # LLM may not have .model attribute
5152
try:
5253
model = llm._model # type: ignore[attr-defined]
5354
if model:
5455
return model
5556
except AttributeError:
56-
pass
57+
pass # LLM may not have ._model attribute
5758
return "unknown"
5859

5960

@@ -72,7 +73,7 @@ def _extract_usage_from_response(resp: Any) -> Dict[str, Optional[int]]:
7273
completion = usage_md.get("output_tokens") or usage_md.get("completion_tokens")
7374
total = usage_md.get("total_tokens")
7475
except AttributeError:
75-
pass
76+
pass # Response may not have .usage_metadata attribute
7677

7778
# Some wrappers place the raw OpenAI usage in response_metadata
7879
if prompt is None or completion is None:
@@ -88,7 +89,7 @@ def _extract_usage_from_response(resp: Any) -> Dict[str, Optional[int]]:
8889
)
8990
total = total or usage.get("total_tokens")
9091
except AttributeError:
91-
pass
92+
pass # Response may not have .response_metadata attribute
9293

9394
# Many providers only give total
9495
if total is None and prompt is not None and completion is not None:

redis_sre_agent/tools/metrics/prometheus/provider.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,8 +226,6 @@ async def _wait_for_targets(self, timeout_seconds: float = 10.0) -> None:
226226
if last_err:
227227
logger.debug(f"_wait_for_targets encountered: {last_err}")
228228

229-
pass
230-
231229
async def _http_get_json(
232230
self, path: str, params: Optional[Dict[str, Any]] = None
233231
) -> Dict[str, Any]:

redis_sre_agent/tools/models.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
from enum import Enum
2-
from typing import Any, Dict, Optional
2+
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, Optional
33

4-
from pydantic import BaseModel, Field
4+
from pydantic import BaseModel, ConfigDict, Field
5+
6+
if TYPE_CHECKING:
7+
from redis_sre_agent.tools.models import ToolDefinition
58

69

710
class ToolCapability(Enum):
@@ -21,8 +24,7 @@ class ToolMetadata(BaseModel):
2124
"""Metadata about a concrete tool implementation.
2225
2326
This is attached to the :class:`Tool` wrapper alongside the
24-
:class:`~redis_sre_agent.tools.tool_definition.ToolDefinition` schema
25-
and the callable used to execute the tool.
27+
:class:`ToolDefinition` schema and the callable used to execute the tool.
2628
"""
2729

2830
name: str
@@ -37,15 +39,16 @@ class Tool(BaseModel):
3739
3840
Attributes:
3941
metadata: :class:`ToolMetadata` describing the tool for routing.
40-
schema: The :class:`ToolDefinition` shown to the LLM (stored as ``Any``
41-
here to avoid import cycles).
42+
schema: The :class:`ToolDefinition` shown to the LLM.
4243
invoke: Async callable taking a single ``Dict[str, Any]`` of arguments
4344
and returning the tool result.
4445
"""
4546

47+
model_config = ConfigDict(arbitrary_types_allowed=True)
48+
4649
metadata: ToolMetadata
47-
schema: Any
48-
invoke: Any
50+
schema: "ToolDefinition"
51+
invoke: Callable[[Dict[str, Any]], Awaitable[Any]]
4952

5053

5154
class SystemHost(BaseModel):

redis_sre_agent/tools/protocols.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,14 @@ def resolve_operation(self, tool_name: str, args: Dict[str, Any]) -> Optional[st
224224
225225
Providers can override this for more complex mappings (for example,
226226
when multiple tool names share a single implementation).
227+
228+
Note: This method is used for OTel tracing and status updates, not for
229+
routing. The ToolManager validates tool existence via the routing table
230+
before this is called.
227231
"""
232+
import logging
233+
234+
_logger = logging.getLogger(__name__)
228235

229236
try:
230237
# Preferred fast-path: exact prefix match based on provider name
@@ -241,8 +248,14 @@ def resolve_operation(self, tool_name: str, args: Dict[str, Any]) -> Optional[st
241248
return "_".join(parts[2:])
242249

243250
# Last resort: treat the whole name as the operation.
251+
# Log a warning since this may indicate a misconfigured tool name.
252+
_logger.warning(
253+
f"resolve_operation falling back to full tool name '{tool_name}' as operation. "
254+
f"Expected format: {self.provider_name}_<hash>_<operation>"
255+
)
244256
return tool_name
245-
except Exception:
257+
except Exception as e:
258+
_logger.warning(f"resolve_operation failed for '{tool_name}': {e}")
246259
return None
247260

248261
@property
@@ -363,10 +376,8 @@ def tools(self) -> List["Tool"]:
363376
requires_instance=requires_instance,
364377
)
365378

366-
# Prefer a direct provider method derived from the tool name.
379+
# Derive operation name and look up the corresponding method.
367380
op_name = self.resolve_operation(schema.name, {}) or ""
368-
# Check if method exists on the class
369-
# Use getattr for legitimate metaprogramming - dynamically binding methods
370381
method = getattr(self, op_name, None) if op_name else None
371382

372383
if not callable(method):
@@ -376,8 +387,8 @@ def tools(self) -> List["Tool"]:
376387
"or override tools() to provide a custom implementation."
377388
)
378389

390+
# Capture method via default arg to avoid late binding in the closure.
379391
async def _invoke(args: Dict[str, Any], _method=method) -> Any:
380-
"""Invoke the bound provider method with keyword args."""
381392
return await _method(**(args or {}))
382393

383394
tools.append(Tool(metadata=meta, schema=schema, invoke=_invoke))

0 commit comments

Comments
 (0)