Coverage for core / labeled_tool.py: 90.0%

10 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-05-12 04:49 +0000

1"""LabeledTool factory — Tool construction with mandatory UI label (#508). 

2 

3Every Tool() that appears in the chat agent's tool registry should be 

4constructed via labeled_tool() instead of the bare langchain Tool(). 

5Required `ui_label` kwarg → Python raises TypeError at construction if a 

6new tool is added without supplying user-facing status text. 

7 

8This is the compile-time replacement for an AST drift-guard test: 

9the constraint lives in the type system, not in a sibling regex check. 

10 

11Coverage spans both static and dynamic tool sources: 

12 

13 - Hardcoded literals in hart_intelligence_entry.get_tools(is_first=True) 

14 - integrations.skills.registry (HART skills loaded from disk) 

15 - integrations.service_tools.registry (HTTP microservice tools) 

16 - integrations.providers.agent_tools (provider gateway) 

17 - integrations.service_tools.system_introspect_tool 

18 

19Each site supplies a ui_label appropriate to the tool. Dynamic 

20registries that can't pre-compute a friendly label may pass the 

21generic_label() helper which returns "Running {name}…" — explicit 

22opt-in to the fallback, not silent drift. 

23 

24The returned object is an unmodified langchain Tool — the factory adds 

25no runtime overhead. TOOL_LABELS dict is the single source of truth 

26for the spinner's CyclingVerb override; this factory just ensures every 

27construction site populates it. 

28""" 

29from __future__ import annotations 

30 

31from typing import Any, Callable 

32 

33# Note: `from langchain_classic.agents import Tool` is INTENTIONALLY deferred 

34# to function-body — module-load eager import would drag langchain (and its 

35# transformers dependency) into every entry path that touches `core.*`, 

36# bypassing the import-graph ordering the transformers re-entry guard 

37# (core/_transformers_lazy_guard.py + hart_intelligence_entry.py:80-209) 

38# relies on. By the time `labeled_tool()` is actually called, the 

39# canonical loader has finished and `langchain_classic.agents` is cached 

40# in sys.modules — the function-body import is then a dict hit, no 

41# `_LazyModule.__getattr__` walk. 

42 

43from core.constants import register_tool_label 

44 

45 

46def labeled_tool( 

47 name: str, 

48 func: Callable[..., Any], 

49 description: str, 

50 *, 

51 ui_label: str, 

52): 

53 """Construct a langchain Tool with a mandatory UI status label. 

54 

55 Args: 

56 name: Tool name as the LLM and `_with_tool_logging` see it. 

57 func: Tool function (single arg → output string). 

58 description: LLM-facing description that drives tool selection. 

59 ui_label: Short user-facing status text ≤ 60 chars shown in the 

60 spinner when this tool fires. REQUIRED — call site must 

61 decide. For tools without a meaningful verb phrase, pass 

62 generic_label(name) to opt explicitly into the fallback. 

63 

64 Returns: 

65 A langchain Tool — drop-in for existing Tool() construction. 

66 

67 Raises: 

68 TypeError: if ui_label is omitted (Python kwarg enforcement). 

69 ValueError: if ui_label is empty or non-string. 

70 """ 

71 if not isinstance(ui_label, str) or not ui_label.strip(): 

72 raise ValueError( 

73 f"labeled_tool({name!r}): ui_label must be a non-empty string; " 

74 f"pass generic_label({name!r}) to opt into the 'Running …' fallback") 

75 register_tool_label(name, ui_label) 

76 # Lazy import: by the time we're here, langchain is already loaded. 

77 from langchain_classic.agents import Tool # noqa: PLC0415 

78 return Tool(name=name, func=func, description=description) 

79 

80 

81def generic_label(name: str) -> str: 

82 """Explicit opt-in to the 'Running {name}…' fallback for tools whose 

83 name is already self-descriptive. Use sparingly — a real verb 

84 phrase (e.g. 'Searching the web…') is usually more polished.""" 

85 return f'Running {name}…'