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
« 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).
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.
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.
11Coverage spans both static and dynamic tool sources:
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
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.
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
31from typing import Any, Callable
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.
43from core.constants import register_tool_label
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.
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.
64 Returns:
65 A langchain Tool — drop-in for existing Tool() construction.
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)
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}…'