unique_toolkit 2026.28.0.dev5__py3-none-any.whl → 2026.28.0.dev7__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.
- unique_toolkit/_common/chunk_relevancy_sorter/service.py +1 -1
- unique_toolkit/agentic/evaluation/context_relevancy/service.py +1 -1
- unique_toolkit/agentic/evaluation/hallucination/constants.py +12 -0
- unique_toolkit/agentic/evaluation/hallucination/hallucination_evaluation.py +11 -0
- unique_toolkit/agentic/evaluation/tests/test_hallucination_external_context.py +122 -0
- unique_toolkit/agentic/reference_manager/reference_manager.py +16 -0
- unique_toolkit/agentic/reference_manager/tests/test_external_context.py +35 -0
- unique_toolkit/agentic/tools/experimental/retrieve_search_scope_tool/tool.py +1 -1
- unique_toolkit/agentic/tools/run_context.py +24 -0
- unique_toolkit/agentic/tools/tool.py +44 -5
- unique_toolkit/agentic/tools/tool_manager.py +147 -20
- unique_toolkit/agentic_table/schemas.py +62 -24
- unique_toolkit/agentic_table/synthetic_chat_event.py +67 -0
- unique_toolkit/app/schemas.py +54 -15
- unique_toolkit/app/unique_settings.py +38 -0
- unique_toolkit/content/schemas.py +9 -1
- unique_toolkit/content/service.py +79 -40
- unique_toolkit/language_model/service.py +21 -21
- unique_toolkit/services/factory.py +10 -2
- unique_toolkit/services/knowledge_base.py +2 -2
- unique_toolkit/short_term_memory/service.py +27 -17
- {unique_toolkit-2026.28.0.dev5.dist-info → unique_toolkit-2026.28.0.dev7.dist-info}/METADATA +2 -2
- {unique_toolkit-2026.28.0.dev5.dist-info → unique_toolkit-2026.28.0.dev7.dist-info}/RECORD +24 -20
- {unique_toolkit-2026.28.0.dev5.dist-info → unique_toolkit-2026.28.0.dev7.dist-info}/WHEEL +0 -0
|
@@ -71,7 +71,7 @@ class ChunkRelevancySorter:
|
|
|
71
71
|
company_id: str | None = None,
|
|
72
72
|
user_id: str | None = None,
|
|
73
73
|
):
|
|
74
|
-
if isinstance(event,
|
|
74
|
+
if isinstance(event, BaseEvent):
|
|
75
75
|
self.chunk_relevancy_evaluator = ContextRelevancyEvaluator.from_event(event)
|
|
76
76
|
else:
|
|
77
77
|
[company_id, user_id] = validate_required_values([company_id, user_id])
|
|
@@ -81,7 +81,7 @@ class ContextRelevancyEvaluator:
|
|
|
81
81
|
company_id: str | None = None,
|
|
82
82
|
user_id: str | None = None,
|
|
83
83
|
):
|
|
84
|
-
if isinstance(event,
|
|
84
|
+
if isinstance(event, BaseEvent):
|
|
85
85
|
self.language_model_service = LanguageModelService.from_event(event)
|
|
86
86
|
else:
|
|
87
87
|
[company_id, user_id] = validate_required_values([company_id, user_id])
|
|
@@ -52,6 +52,18 @@ class HallucinationConfig(EvaluationMetricConfig):
|
|
|
52
52
|
default={},
|
|
53
53
|
description="Additional options to pass to the language model.",
|
|
54
54
|
)
|
|
55
|
+
max_external_context_chars_per_source: SkipJsonSchema[int] = Field(
|
|
56
|
+
default=20_000,
|
|
57
|
+
ge=0,
|
|
58
|
+
description=(
|
|
59
|
+
"Per-source character cap applied to external (non-chunk) "
|
|
60
|
+
"context texts, e.g. MCP tool output. Raw/large tool results "
|
|
61
|
+
"inflate the judge prompt and the eval-LLM call; each external "
|
|
62
|
+
"text is truncated to this many characters before being added "
|
|
63
|
+
"to the context. 0 disables truncation. Does not affect "
|
|
64
|
+
"chunk-derived context."
|
|
65
|
+
),
|
|
66
|
+
)
|
|
55
67
|
score_to_label: dict[str, str] = {
|
|
56
68
|
"LOW": "GREEN",
|
|
57
69
|
"MEDIUM": "YELLOW",
|
|
@@ -54,6 +54,17 @@ class HallucinationEvaluation(Evaluation):
|
|
|
54
54
|
reference_pattern=self.config.reference_pattern,
|
|
55
55
|
)
|
|
56
56
|
|
|
57
|
+
# Append non-chunk groundedness context (e.g. MCP tool output). These
|
|
58
|
+
# are added in full ("all retrieved output as context") rather than via
|
|
59
|
+
# source-marker selection, since they carry no [sourceN] markers.
|
|
60
|
+
external_context_texts = self._reference_manager.get_external_context_texts()
|
|
61
|
+
max_chars = self.config.max_external_context_chars_per_source
|
|
62
|
+
if max_chars > 0:
|
|
63
|
+
external_context_texts = [
|
|
64
|
+
text[:max_chars] for text in external_context_texts
|
|
65
|
+
]
|
|
66
|
+
context_texts = context_texts + external_context_texts
|
|
67
|
+
|
|
57
68
|
evaluation_result: EvaluationMetricResult = await check_hallucination(
|
|
58
69
|
company_id=self._company_id,
|
|
59
70
|
user_id=self._user_id,
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Tests that HallucinationEvaluation grounds against non-chunk context.
|
|
2
|
+
|
|
3
|
+
UN-21951: external context texts (e.g. MCP tool output) registered on the
|
|
4
|
+
ReferenceManager must be appended to the evaluation context — in addition to
|
|
5
|
+
chunk-derived context, capped by ``max_external_context_chars_per_source`` —
|
|
6
|
+
without being converted to ContentChunks.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from types import SimpleNamespace
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
from unique_toolkit.agentic.evaluation.hallucination import (
|
|
14
|
+
hallucination_evaluation as he,
|
|
15
|
+
)
|
|
16
|
+
from unique_toolkit.agentic.evaluation.hallucination.constants import (
|
|
17
|
+
HallucinationConfig,
|
|
18
|
+
SourceSelectionMode,
|
|
19
|
+
)
|
|
20
|
+
from unique_toolkit.agentic.evaluation.schemas import (
|
|
21
|
+
EvaluationMetricName,
|
|
22
|
+
EvaluationMetricResult,
|
|
23
|
+
)
|
|
24
|
+
from unique_toolkit.agentic.reference_manager.reference_manager import (
|
|
25
|
+
ReferenceManager,
|
|
26
|
+
)
|
|
27
|
+
from unique_toolkit.chat.schemas import ChatMessageRole
|
|
28
|
+
from unique_toolkit.language_model.schemas import (
|
|
29
|
+
LanguageModelStreamResponse,
|
|
30
|
+
LanguageModelStreamResponseMessage,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _make_event() -> SimpleNamespace:
|
|
35
|
+
return SimpleNamespace(
|
|
36
|
+
company_id="company_1",
|
|
37
|
+
user_id="user_1",
|
|
38
|
+
payload=SimpleNamespace(
|
|
39
|
+
user_message=SimpleNamespace(text="what is ACME revenue?"),
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _make_response(text: str = "ACME revenue is 1M.") -> LanguageModelStreamResponse:
|
|
45
|
+
return LanguageModelStreamResponse(
|
|
46
|
+
message=LanguageModelStreamResponseMessage(
|
|
47
|
+
id="msg_1",
|
|
48
|
+
previous_message_id=None,
|
|
49
|
+
role=ChatMessageRole.ASSISTANT,
|
|
50
|
+
text=text,
|
|
51
|
+
chat_id="chat_1",
|
|
52
|
+
original_text=text,
|
|
53
|
+
references=None,
|
|
54
|
+
),
|
|
55
|
+
tool_calls=None,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@pytest.fixture
|
|
60
|
+
def captured_context(monkeypatch: pytest.MonkeyPatch) -> dict:
|
|
61
|
+
"""Patch check_hallucination to capture the context_texts it receives."""
|
|
62
|
+
captured: dict = {}
|
|
63
|
+
|
|
64
|
+
async def _fake_check_hallucination(*, company_id, user_id, input, config):
|
|
65
|
+
captured["context_texts"] = list(input.context_texts or [])
|
|
66
|
+
return EvaluationMetricResult(
|
|
67
|
+
name=EvaluationMetricName.HALLUCINATION,
|
|
68
|
+
value="LOW",
|
|
69
|
+
reason="grounded",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
monkeypatch.setattr(he, "check_hallucination", _fake_check_hallucination)
|
|
73
|
+
return captured
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
async def test_external_context_is_appended(captured_context: dict) -> None:
|
|
77
|
+
manager = ReferenceManager()
|
|
78
|
+
manager.add_external_context_texts(["ACME revenue: 1M"])
|
|
79
|
+
evaluation = he.HallucinationEvaluation(
|
|
80
|
+
HallucinationConfig(source_selection_mode=SourceSelectionMode.FROM_IDS),
|
|
81
|
+
_make_event(), # type: ignore[arg-type]
|
|
82
|
+
manager,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
result = await evaluation.run(_make_response())
|
|
86
|
+
|
|
87
|
+
# No chunks → context comes solely from the external (MCP) source.
|
|
88
|
+
assert captured_context["context_texts"] == ["ACME revenue: 1M"]
|
|
89
|
+
assert result.value == "LOW"
|
|
90
|
+
assert result.is_positive is True
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
async def test_external_context_is_truncated(captured_context: dict) -> None:
|
|
94
|
+
manager = ReferenceManager()
|
|
95
|
+
manager.add_external_context_texts(["x" * 100])
|
|
96
|
+
evaluation = he.HallucinationEvaluation(
|
|
97
|
+
HallucinationConfig(
|
|
98
|
+
source_selection_mode=SourceSelectionMode.FROM_IDS,
|
|
99
|
+
max_external_context_chars_per_source=10,
|
|
100
|
+
),
|
|
101
|
+
_make_event(), # type: ignore[arg-type]
|
|
102
|
+
manager,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
await evaluation.run(_make_response())
|
|
106
|
+
|
|
107
|
+
assert captured_context["context_texts"] == ["x" * 10]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
async def test_no_external_context_preserves_chunk_only_behaviour(
|
|
111
|
+
captured_context: dict,
|
|
112
|
+
) -> None:
|
|
113
|
+
manager = ReferenceManager() # no chunks, no external context
|
|
114
|
+
evaluation = he.HallucinationEvaluation(
|
|
115
|
+
HallucinationConfig(source_selection_mode=SourceSelectionMode.FROM_IDS),
|
|
116
|
+
_make_event(), # type: ignore[arg-type]
|
|
117
|
+
manager,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
await evaluation.run(_make_response())
|
|
121
|
+
|
|
122
|
+
assert captured_context["context_texts"] == []
|
|
@@ -32,6 +32,7 @@ class ReferenceManager:
|
|
|
32
32
|
self._tool_chunks: dict[str, tool_chunks] = {}
|
|
33
33
|
self._chunks: list[ContentChunk] = []
|
|
34
34
|
self._references: list[list[ContentReference]] = []
|
|
35
|
+
self._external_context_texts: list[str] = []
|
|
35
36
|
|
|
36
37
|
def extract_referenceable_chunks(
|
|
37
38
|
self, tool_responses: list[ToolCallResponse]
|
|
@@ -47,6 +48,21 @@ class ReferenceManager:
|
|
|
47
48
|
def get_chunks(self) -> list[ContentChunk]:
|
|
48
49
|
return self._chunks
|
|
49
50
|
|
|
51
|
+
def add_external_context_texts(self, texts: list[str]) -> None:
|
|
52
|
+
"""Register groundedness context that is not backed by ContentChunks.
|
|
53
|
+
|
|
54
|
+
Some retrieval sources (e.g. MCP tool output) provide text the answer
|
|
55
|
+
is built on but are deliberately *not* modelled as ContentChunks — they
|
|
56
|
+
are a different entity from ingested document chunks and must not flow
|
|
57
|
+
through the chunk/reference pipeline. They still need to ground the
|
|
58
|
+
hallucination check, so they are stored here and appended to the
|
|
59
|
+
evaluation context alongside the chunk-derived texts.
|
|
60
|
+
"""
|
|
61
|
+
self._external_context_texts.extend(text for text in texts if text)
|
|
62
|
+
|
|
63
|
+
def get_external_context_texts(self) -> list[str]:
|
|
64
|
+
return self._external_context_texts
|
|
65
|
+
|
|
50
66
|
def get_tool_chunks(self) -> dict[str, tool_chunks]:
|
|
51
67
|
return self._tool_chunks
|
|
52
68
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Tests for non-chunk groundedness context on the ReferenceManager.
|
|
2
|
+
|
|
3
|
+
External context texts (e.g. MCP tool output, UN-21951) ground the
|
|
4
|
+
hallucination check without being modelled as ContentChunks.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from unique_toolkit.agentic.reference_manager.reference_manager import (
|
|
8
|
+
ReferenceManager,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_external_context_starts_empty() -> None:
|
|
13
|
+
manager = ReferenceManager()
|
|
14
|
+
assert manager.get_external_context_texts() == []
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_add_external_context_texts_accumulates() -> None:
|
|
18
|
+
manager = ReferenceManager()
|
|
19
|
+
manager.add_external_context_texts(["a", "b"])
|
|
20
|
+
manager.add_external_context_texts(["c"])
|
|
21
|
+
assert manager.get_external_context_texts() == ["a", "b", "c"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_add_external_context_texts_drops_empty_strings() -> None:
|
|
25
|
+
manager = ReferenceManager()
|
|
26
|
+
manager.add_external_context_texts(["keep", "", "also"])
|
|
27
|
+
assert manager.get_external_context_texts() == ["keep", "also"]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_external_context_is_independent_of_chunks() -> None:
|
|
31
|
+
manager = ReferenceManager()
|
|
32
|
+
manager.add_external_context_texts(["mcp output"])
|
|
33
|
+
# External context must not leak into the chunk pipeline.
|
|
34
|
+
assert manager.get_chunks() == []
|
|
35
|
+
assert manager.get_external_context_texts() == ["mcp output"]
|
|
@@ -116,7 +116,7 @@ class RetrieveSearchScopeTool(Tool[RetrieveSearchScopeConfig]):
|
|
|
116
116
|
)
|
|
117
117
|
|
|
118
118
|
try:
|
|
119
|
-
settings = UniqueSettings.from_chat_event(self.
|
|
119
|
+
settings = UniqueSettings.from_chat_event(self.event)
|
|
120
120
|
kb_service = UniqueServiceFactory(
|
|
121
121
|
settings=settings
|
|
122
122
|
).knowledge_base_service()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
4
|
+
from typing_extensions import Self
|
|
5
|
+
|
|
6
|
+
from unique_toolkit.app.schemas import ChatEvent
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ToolRunContext(BaseModel):
|
|
10
|
+
"""Tool-manager runtime state decoupled from a raw :class:`ChatEvent`."""
|
|
11
|
+
|
|
12
|
+
model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)
|
|
13
|
+
|
|
14
|
+
tool_choices: list[str] = Field(default_factory=list)
|
|
15
|
+
disabled_tools: list[str] = Field(default_factory=list)
|
|
16
|
+
tool_init_event: ChatEvent | None = None
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def from_chat_event(cls, event: ChatEvent) -> Self:
|
|
20
|
+
return cls(
|
|
21
|
+
tool_choices=list(event.payload.tool_choices),
|
|
22
|
+
disabled_tools=list(event.payload.disabled_tools),
|
|
23
|
+
tool_init_event=event,
|
|
24
|
+
)
|
|
@@ -18,7 +18,8 @@ from unique_toolkit.app.schemas import ChatEvent
|
|
|
18
18
|
from unique_toolkit.language_model import LanguageModelToolDescription
|
|
19
19
|
|
|
20
20
|
if TYPE_CHECKING:
|
|
21
|
-
|
|
21
|
+
from unique_toolkit.language_model.service import LanguageModelService
|
|
22
|
+
from unique_toolkit.services.chat_service import ChatService
|
|
22
23
|
from unique_toolkit.language_model.schemas import (
|
|
23
24
|
LanguageModelFunction,
|
|
24
25
|
)
|
|
@@ -169,6 +170,9 @@ class Tool(ABC, Generic[ConfigType]):
|
|
|
169
170
|
def __init__(self, config: ConfigType) -> None: ...
|
|
170
171
|
|
|
171
172
|
@overload
|
|
173
|
+
@deprecated(
|
|
174
|
+
"Passing event is deprecated. Use Tool(config) and inject context in run()."
|
|
175
|
+
)
|
|
172
176
|
def __init__(
|
|
173
177
|
self,
|
|
174
178
|
config: ConfigType,
|
|
@@ -176,17 +180,32 @@ class Tool(ABC, Generic[ConfigType]):
|
|
|
176
180
|
tool_progress_reporter: ToolProgressReporter | None = ...,
|
|
177
181
|
) -> None: ...
|
|
178
182
|
|
|
183
|
+
@overload
|
|
184
|
+
def __init__(
|
|
185
|
+
self,
|
|
186
|
+
config: ConfigType,
|
|
187
|
+
*,
|
|
188
|
+
chat_service: ChatService,
|
|
189
|
+
language_model_service: LanguageModelService,
|
|
190
|
+
tool_progress_reporter: ToolProgressReporter | None = ...,
|
|
191
|
+
) -> None: ...
|
|
192
|
+
|
|
179
193
|
def __init__(
|
|
180
194
|
self,
|
|
181
195
|
config: ConfigType,
|
|
182
196
|
event: ChatEvent | None = None,
|
|
183
197
|
tool_progress_reporter: ToolProgressReporter | None = None,
|
|
198
|
+
*,
|
|
199
|
+
chat_service: ChatService | None = None,
|
|
200
|
+
language_model_service: LanguageModelService | None = None,
|
|
184
201
|
) -> None:
|
|
185
202
|
"""Initialize the tool.
|
|
186
203
|
|
|
187
|
-
Preferred (decoupled):
|
|
204
|
+
Preferred (decoupled): ``Tool(config)`` — configuration only.
|
|
188
205
|
|
|
189
|
-
|
|
206
|
+
Service injection: ``Tool(config, chat_service=..., language_model_service=...)``.
|
|
207
|
+
|
|
208
|
+
Backward compatible: ``Tool(config, event, tool_progress_reporter)`` — creates
|
|
190
209
|
deprecated chat_service, language_model_service, message_step_logger for
|
|
191
210
|
legacy subclasses.
|
|
192
211
|
"""
|
|
@@ -200,6 +219,25 @@ class Tool(ABC, Generic[ConfigType]):
|
|
|
200
219
|
self.logger = getLogger(f"{module_name}.{__name__}")
|
|
201
220
|
self.debug_info: dict[str, Any] = {}
|
|
202
221
|
|
|
222
|
+
if (chat_service is None) != (language_model_service is None):
|
|
223
|
+
raise ValueError(
|
|
224
|
+
"chat_service and language_model_service must be injected together; "
|
|
225
|
+
"supplying only one is not supported."
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
if chat_service is not None and language_model_service is not None:
|
|
229
|
+
from unique_toolkit.agentic.message_log_manager.service import (
|
|
230
|
+
MessageStepLogger,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
self._tool_progress_reporter = tool_progress_reporter
|
|
234
|
+
self._chat_service = chat_service
|
|
235
|
+
self._language_model_service = language_model_service
|
|
236
|
+
self._message_step_logger = MessageStepLogger(
|
|
237
|
+
chat_service=self._chat_service,
|
|
238
|
+
)
|
|
239
|
+
return
|
|
240
|
+
|
|
203
241
|
if event is not None:
|
|
204
242
|
from unique_toolkit.agentic.message_log_manager.service import (
|
|
205
243
|
MessageStepLogger,
|
|
@@ -220,11 +258,12 @@ class Tool(ABC, Generic[ConfigType]):
|
|
|
220
258
|
"Never reuse event. Dangerous. Prefer Tool(config) and inject context in run()."
|
|
221
259
|
)
|
|
222
260
|
def event(self) -> ChatEvent:
|
|
223
|
-
|
|
261
|
+
chat_event = getattr(self, "_event", None)
|
|
262
|
+
if chat_event is None:
|
|
224
263
|
raise AttributeError(
|
|
225
264
|
"event not available (tool was initialized with config only). "
|
|
226
265
|
)
|
|
227
|
-
return
|
|
266
|
+
return chat_event
|
|
228
267
|
|
|
229
268
|
@property
|
|
230
269
|
@deprecated(
|
|
@@ -12,6 +12,7 @@ from openai.types.responses import (
|
|
|
12
12
|
response_create_params,
|
|
13
13
|
)
|
|
14
14
|
from pydantic import BaseModel, Field
|
|
15
|
+
from typing_extensions import Self
|
|
15
16
|
|
|
16
17
|
from unique_toolkit._common.execution import (
|
|
17
18
|
Result,
|
|
@@ -31,6 +32,7 @@ from unique_toolkit.agentic.tools.openai_builtin.base import (
|
|
|
31
32
|
OpenAIBuiltInToolName,
|
|
32
33
|
)
|
|
33
34
|
from unique_toolkit.agentic.tools.openai_builtin.manager import OpenAIBuiltInToolManager
|
|
35
|
+
from unique_toolkit.agentic.tools.run_context import ToolRunContext
|
|
34
36
|
from unique_toolkit.agentic.tools.schemas import ToolCallResponse, ToolPrompts
|
|
35
37
|
from unique_toolkit.agentic.tools.tool import Tool
|
|
36
38
|
from unique_toolkit.agentic.tools.tool_progress_reporter import ToolProgressReporter
|
|
@@ -88,13 +90,62 @@ class _ToolManager(Generic[_ApiMode]):
|
|
|
88
90
|
a2a_manager: A2AManager,
|
|
89
91
|
api_mode: _ApiMode,
|
|
90
92
|
builtin_tool_manager: OpenAIBuiltInToolManager | None = None,
|
|
93
|
+
) -> None:
|
|
94
|
+
self._setup(
|
|
95
|
+
logger=logger,
|
|
96
|
+
config=config,
|
|
97
|
+
run_context=ToolRunContext.from_chat_event(event),
|
|
98
|
+
tool_progress_reporter=tool_progress_reporter,
|
|
99
|
+
mcp_manager=mcp_manager,
|
|
100
|
+
a2a_manager=a2a_manager,
|
|
101
|
+
api_mode=api_mode,
|
|
102
|
+
builtin_tool_manager=builtin_tool_manager,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def from_run_context(
|
|
107
|
+
cls,
|
|
108
|
+
logger: Logger,
|
|
109
|
+
config: ToolManagerConfig,
|
|
110
|
+
*,
|
|
111
|
+
run_context: ToolRunContext,
|
|
112
|
+
tool_progress_reporter: ToolProgressReporter,
|
|
113
|
+
mcp_manager: MCPManager,
|
|
114
|
+
a2a_manager: A2AManager,
|
|
115
|
+
api_mode: _ApiMode,
|
|
116
|
+
builtin_tool_manager: OpenAIBuiltInToolManager | None = None,
|
|
117
|
+
) -> Self:
|
|
118
|
+
instance = cls.__new__(cls)
|
|
119
|
+
instance._setup(
|
|
120
|
+
logger=logger,
|
|
121
|
+
config=config,
|
|
122
|
+
run_context=run_context,
|
|
123
|
+
tool_progress_reporter=tool_progress_reporter,
|
|
124
|
+
mcp_manager=mcp_manager,
|
|
125
|
+
a2a_manager=a2a_manager,
|
|
126
|
+
api_mode=api_mode,
|
|
127
|
+
builtin_tool_manager=builtin_tool_manager,
|
|
128
|
+
)
|
|
129
|
+
return instance
|
|
130
|
+
|
|
131
|
+
def _setup(
|
|
132
|
+
self,
|
|
133
|
+
*,
|
|
134
|
+
logger: Logger,
|
|
135
|
+
config: ToolManagerConfig,
|
|
136
|
+
run_context: ToolRunContext,
|
|
137
|
+
tool_progress_reporter: ToolProgressReporter,
|
|
138
|
+
mcp_manager: MCPManager,
|
|
139
|
+
a2a_manager: A2AManager,
|
|
140
|
+
api_mode: _ApiMode,
|
|
141
|
+
builtin_tool_manager: OpenAIBuiltInToolManager | None,
|
|
91
142
|
) -> None:
|
|
92
143
|
self._logger = logger
|
|
93
144
|
self._config = config
|
|
94
145
|
self._tool_progress_reporter = tool_progress_reporter
|
|
95
146
|
self._tools: list[Tool[Any] | OpenAIBuiltInTool[Any]] = []
|
|
96
|
-
self._tool_choices =
|
|
97
|
-
self._disabled_tools =
|
|
147
|
+
self._tool_choices = run_context.tool_choices
|
|
148
|
+
self._disabled_tools = run_context.disabled_tools
|
|
98
149
|
self._exclusive_tools = [
|
|
99
150
|
tool.name for tool in self._config.tools if tool.is_exclusive
|
|
100
151
|
]
|
|
@@ -104,17 +155,33 @@ class _ToolManager(Generic[_ApiMode]):
|
|
|
104
155
|
self._a2a_manager = a2a_manager
|
|
105
156
|
self._builtin_tool_manager = builtin_tool_manager
|
|
106
157
|
self._api_mode = api_mode
|
|
107
|
-
self.
|
|
108
|
-
|
|
109
|
-
|
|
158
|
+
self._sub_agents: list[SubAgentTool] = []
|
|
159
|
+
self._builtin_tools: list[OpenAIBuiltInTool[Any]] = []
|
|
160
|
+
self._mcp_tools: list[Tool[Any]] = []
|
|
161
|
+
self._internal_tools: list[Tool[Any]] = []
|
|
162
|
+
self.available_tools: list[
|
|
163
|
+
Tool[Any] | OpenAIBuiltInTool[Any] | SubAgentTool
|
|
164
|
+
] = []
|
|
165
|
+
self._init__tools(run_context.tool_init_event)
|
|
166
|
+
|
|
167
|
+
def _init__tools(self, tool_init_event: ChatEvent | None) -> None:
|
|
110
168
|
tool_choices = self._tool_choices
|
|
111
169
|
tool_configs = self._config.tools
|
|
112
170
|
self._logger.info("Initializing tool definitions...")
|
|
113
171
|
self._logger.info(f"Tool choices: {tool_choices}")
|
|
114
172
|
|
|
115
|
-
|
|
116
|
-
tool_configs,
|
|
117
|
-
|
|
173
|
+
if tool_init_event is not None:
|
|
174
|
+
tool_configs, sub_agents = self._a2a_manager.get_all_sub_agents(
|
|
175
|
+
tool_configs,
|
|
176
|
+
tool_init_event,
|
|
177
|
+
)
|
|
178
|
+
else:
|
|
179
|
+
tool_configs = [
|
|
180
|
+
tool_config
|
|
181
|
+
for tool_config in tool_configs
|
|
182
|
+
if not tool_config.is_sub_agent
|
|
183
|
+
]
|
|
184
|
+
sub_agents = []
|
|
118
185
|
self._sub_agents = sub_agents
|
|
119
186
|
|
|
120
187
|
registered_tool_names = set(t.name for t in self._sub_agents)
|
|
@@ -133,18 +200,27 @@ class _ToolManager(Generic[_ApiMode]):
|
|
|
133
200
|
registered_tool_names.update(t.name for t in self._mcp_tools)
|
|
134
201
|
|
|
135
202
|
# Build internal tools from configurations
|
|
136
|
-
self._internal_tools
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
203
|
+
self._internal_tools.clear()
|
|
204
|
+
for t in tool_configs:
|
|
205
|
+
if t.name in registered_tool_names:
|
|
206
|
+
continue
|
|
207
|
+
if t.name in OpenAIBuiltInToolName:
|
|
208
|
+
continue
|
|
209
|
+
if tool_init_event is not None:
|
|
210
|
+
built_tool = ToolFactory.build_tool_with_settings(
|
|
211
|
+
t.name,
|
|
212
|
+
t,
|
|
213
|
+
t.configuration,
|
|
214
|
+
tool_init_event,
|
|
215
|
+
tool_progress_reporter=self._tool_progress_reporter,
|
|
216
|
+
)
|
|
217
|
+
else:
|
|
218
|
+
self._logger.info(
|
|
219
|
+
"Skipping internal tool '%s' (requires chat event for initialization)",
|
|
220
|
+
t.name,
|
|
221
|
+
)
|
|
222
|
+
continue
|
|
223
|
+
self._internal_tools.append(built_tool)
|
|
148
224
|
|
|
149
225
|
# Combine all types of tools
|
|
150
226
|
self.available_tools = (
|
|
@@ -607,6 +683,30 @@ class ToolManager(_ToolManager[Literal["completions"]]):
|
|
|
607
683
|
builtin_tool_manager=None,
|
|
608
684
|
)
|
|
609
685
|
|
|
686
|
+
@classmethod
|
|
687
|
+
def from_run_context(
|
|
688
|
+
cls,
|
|
689
|
+
logger: Logger,
|
|
690
|
+
config: ToolManagerConfig,
|
|
691
|
+
*,
|
|
692
|
+
run_context: ToolRunContext,
|
|
693
|
+
tool_progress_reporter: ToolProgressReporter,
|
|
694
|
+
mcp_manager: MCPManager,
|
|
695
|
+
a2a_manager: A2AManager,
|
|
696
|
+
api_mode: Literal["completions"] = "completions",
|
|
697
|
+
builtin_tool_manager: OpenAIBuiltInToolManager | None = None,
|
|
698
|
+
) -> Self:
|
|
699
|
+
return super().from_run_context(
|
|
700
|
+
logger=logger,
|
|
701
|
+
config=config,
|
|
702
|
+
run_context=run_context,
|
|
703
|
+
tool_progress_reporter=tool_progress_reporter,
|
|
704
|
+
mcp_manager=mcp_manager,
|
|
705
|
+
a2a_manager=a2a_manager,
|
|
706
|
+
api_mode=api_mode,
|
|
707
|
+
builtin_tool_manager=builtin_tool_manager,
|
|
708
|
+
)
|
|
709
|
+
|
|
610
710
|
|
|
611
711
|
class ResponsesApiToolManager(_ToolManager[Literal["responses"]]):
|
|
612
712
|
def __init__(
|
|
@@ -631,6 +731,33 @@ class ResponsesApiToolManager(_ToolManager[Literal["responses"]]):
|
|
|
631
731
|
builtin_tool_manager=builtin_tool_manager,
|
|
632
732
|
)
|
|
633
733
|
|
|
734
|
+
@classmethod
|
|
735
|
+
def from_run_context(
|
|
736
|
+
cls,
|
|
737
|
+
logger: Logger,
|
|
738
|
+
config: ToolManagerConfig,
|
|
739
|
+
*,
|
|
740
|
+
run_context: ToolRunContext,
|
|
741
|
+
tool_progress_reporter: ToolProgressReporter,
|
|
742
|
+
mcp_manager: MCPManager,
|
|
743
|
+
a2a_manager: A2AManager,
|
|
744
|
+
api_mode: Literal["responses"] = "responses",
|
|
745
|
+
builtin_tool_manager: OpenAIBuiltInToolManager | None = None,
|
|
746
|
+
) -> Self:
|
|
747
|
+
if builtin_tool_manager is None:
|
|
748
|
+
msg = "builtin_tool_manager is required for ResponsesApiToolManager"
|
|
749
|
+
raise ValueError(msg)
|
|
750
|
+
return super().from_run_context(
|
|
751
|
+
logger=logger,
|
|
752
|
+
config=config,
|
|
753
|
+
run_context=run_context,
|
|
754
|
+
tool_progress_reporter=tool_progress_reporter,
|
|
755
|
+
mcp_manager=mcp_manager,
|
|
756
|
+
a2a_manager=a2a_manager,
|
|
757
|
+
api_mode=api_mode,
|
|
758
|
+
builtin_tool_manager=builtin_tool_manager,
|
|
759
|
+
)
|
|
760
|
+
|
|
634
761
|
def get_required_include_params(self) -> list[ResponseIncludable]:
|
|
635
762
|
"""Return Responses API include params required by all active built-in tools."""
|
|
636
763
|
return self._builtin_tool_manager.get_required_include_params()
|