unique_toolkit 1.31.2__py3-none-any.whl → 1.32.1__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/agentic/tools/a2a/postprocessing/_display_utils.py +15 -5
- unique_toolkit/agentic/tools/a2a/postprocessing/_ref_utils.py +11 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/display.py +66 -11
- unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display.py +421 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display_utils.py +380 -0
- unique_toolkit/chat/functions.py +1 -1
- unique_toolkit/content/functions.py +4 -4
- unique_toolkit/content/service.py +1 -1
- unique_toolkit/embedding/service.py +1 -1
- unique_toolkit/framework_utilities/langchain/__init__.py +10 -0
- unique_toolkit/framework_utilities/openai/client.py +2 -1
- unique_toolkit/services/knowledge_base.py +4 -6
- {unique_toolkit-1.31.2.dist-info → unique_toolkit-1.32.1.dist-info}/METADATA +7 -1
- {unique_toolkit-1.31.2.dist-info → unique_toolkit-1.32.1.dist-info}/RECORD +16 -14
- {unique_toolkit-1.31.2.dist-info → unique_toolkit-1.32.1.dist-info}/LICENSE +0 -0
- {unique_toolkit-1.31.2.dist-info → unique_toolkit-1.32.1.dist-info}/WHEEL +0 -0
|
@@ -186,6 +186,18 @@ def get_sub_agent_answer_parts(
|
|
|
186
186
|
return substrings
|
|
187
187
|
|
|
188
188
|
|
|
189
|
+
def get_sub_agent_answer_from_parts(
|
|
190
|
+
answer_parts: list[SubAgentAnswerPart],
|
|
191
|
+
config: SubAgentDisplayConfig,
|
|
192
|
+
) -> str:
|
|
193
|
+
return render_template(
|
|
194
|
+
config.answer_substrings_jinja_template,
|
|
195
|
+
{
|
|
196
|
+
"substrings": [answer.formatted_text for answer in answer_parts],
|
|
197
|
+
},
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
189
201
|
def get_sub_agent_answer_display(
|
|
190
202
|
display_name: str,
|
|
191
203
|
display_config: SubAgentDisplayConfig,
|
|
@@ -200,11 +212,9 @@ def get_sub_agent_answer_display(
|
|
|
200
212
|
)
|
|
201
213
|
|
|
202
214
|
if isinstance(answer, list):
|
|
203
|
-
answer =
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
"substrings": [answer.formatted_text for answer in answer],
|
|
207
|
-
},
|
|
215
|
+
answer = get_sub_agent_answer_from_parts(
|
|
216
|
+
answer_parts=answer,
|
|
217
|
+
config=display_config,
|
|
208
218
|
)
|
|
209
219
|
|
|
210
220
|
return template.format(
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import re
|
|
1
2
|
from typing import Callable, Iterable, Mapping, Sequence
|
|
2
3
|
|
|
3
4
|
from unique_toolkit._common.referencing import get_reference_pattern
|
|
@@ -50,6 +51,16 @@ def add_content_refs(
|
|
|
50
51
|
return message_refs
|
|
51
52
|
|
|
52
53
|
|
|
54
|
+
def remove_unused_refs(
|
|
55
|
+
references: Sequence[ContentReference],
|
|
56
|
+
text: str,
|
|
57
|
+
ref_pattern_f: Callable[[int], str] = get_reference_pattern,
|
|
58
|
+
) -> list[ContentReference]:
|
|
59
|
+
return [
|
|
60
|
+
ref for ref in references if re.search(ref_pattern_f(ref.sequence_number), text)
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
|
|
53
64
|
def add_content_refs_and_replace_in_text(
|
|
54
65
|
message_text: str,
|
|
55
66
|
message_refs: Sequence[ContentReference],
|
|
@@ -10,12 +10,15 @@ from unique_toolkit._common.pydantic_helpers import get_configuration_dict
|
|
|
10
10
|
from unique_toolkit._common.utils.jinja.render import render_template
|
|
11
11
|
from unique_toolkit.agentic.postprocessor.postprocessor_manager import Postprocessor
|
|
12
12
|
from unique_toolkit.agentic.tools.a2a.postprocessing._display_utils import (
|
|
13
|
+
SubAgentAnswerPart,
|
|
13
14
|
get_sub_agent_answer_display,
|
|
15
|
+
get_sub_agent_answer_from_parts,
|
|
14
16
|
get_sub_agent_answer_parts,
|
|
15
17
|
remove_sub_agent_answer_from_text,
|
|
16
18
|
)
|
|
17
19
|
from unique_toolkit.agentic.tools.a2a.postprocessing._ref_utils import (
|
|
18
20
|
add_content_refs_and_replace_in_text,
|
|
21
|
+
remove_unused_refs,
|
|
19
22
|
)
|
|
20
23
|
from unique_toolkit.agentic.tools.a2a.postprocessing.config import (
|
|
21
24
|
SubAgentDisplayConfig,
|
|
@@ -56,6 +59,10 @@ class SubAgentResponsesPostprocessorConfig(BaseModel):
|
|
|
56
59
|
default=_ANSWERS_JINJA_TEMPLATE,
|
|
57
60
|
description="The template to use to display the sub agent answers.",
|
|
58
61
|
)
|
|
62
|
+
filter_duplicate_answers: bool = Field(
|
|
63
|
+
default=True,
|
|
64
|
+
description="If set, duplicate answers will be filtered out.",
|
|
65
|
+
)
|
|
59
66
|
|
|
60
67
|
|
|
61
68
|
class SubAgentResponsesDisplayPostprocessor(Postprocessor):
|
|
@@ -108,23 +115,24 @@ class SubAgentResponsesDisplayPostprocessor(Postprocessor):
|
|
|
108
115
|
|
|
109
116
|
answers_displayed_before = []
|
|
110
117
|
answers_displayed_after = []
|
|
118
|
+
all_displayed_answers = set()
|
|
111
119
|
|
|
112
120
|
for assistant_id, responses in displayed_sub_agent_responses.items():
|
|
121
|
+
tool_info = self._display_specs[assistant_id]
|
|
122
|
+
tool_name = tool_info.display_name
|
|
123
|
+
|
|
113
124
|
for response in responses:
|
|
114
125
|
message = response.message
|
|
115
|
-
tool_info = self._display_specs[assistant_id]
|
|
116
|
-
|
|
117
|
-
_add_response_references_to_message_in_place(
|
|
118
|
-
loop_response=loop_response, response=message
|
|
119
|
-
)
|
|
120
126
|
|
|
121
127
|
if tool_info.display_config.mode == SubAgentResponseDisplayMode.HIDDEN:
|
|
128
|
+
# Add references and continue
|
|
129
|
+
_add_response_references_to_message_in_place(
|
|
130
|
+
loop_response=loop_response,
|
|
131
|
+
response=message,
|
|
132
|
+
remove_unused_references=False,
|
|
133
|
+
)
|
|
122
134
|
continue
|
|
123
135
|
|
|
124
|
-
display_name = tool_info.display_name
|
|
125
|
-
if len(responses) > 1:
|
|
126
|
-
display_name += f" {response.sequence_number}"
|
|
127
|
-
|
|
128
136
|
if message["text"] is None:
|
|
129
137
|
logger.warning(
|
|
130
138
|
"Sub agent response for assistant %s with sequence number %s does not contain any text",
|
|
@@ -138,13 +146,37 @@ class SubAgentResponsesDisplayPostprocessor(Postprocessor):
|
|
|
138
146
|
display_config=tool_info.display_config,
|
|
139
147
|
)
|
|
140
148
|
|
|
149
|
+
if self._config.filter_duplicate_answers:
|
|
150
|
+
answer_parts, all_displayed_answers = (
|
|
151
|
+
_filter_and_update_duplicate_answers(
|
|
152
|
+
answers=answer_parts,
|
|
153
|
+
existing_answers=all_displayed_answers,
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
answer = get_sub_agent_answer_from_parts(
|
|
158
|
+
answer_parts=answer_parts,
|
|
159
|
+
config=tool_info.display_config,
|
|
160
|
+
)
|
|
161
|
+
message["text"] = answer
|
|
162
|
+
|
|
163
|
+
_add_response_references_to_message_in_place(
|
|
164
|
+
loop_response=loop_response,
|
|
165
|
+
response=message,
|
|
166
|
+
remove_unused_references=not tool_info.display_config.force_include_references,
|
|
167
|
+
)
|
|
168
|
+
|
|
141
169
|
if len(answer_parts) == 0:
|
|
142
170
|
continue
|
|
143
171
|
|
|
172
|
+
display_name = tool_name
|
|
173
|
+
if len(responses) > 1:
|
|
174
|
+
display_name = tool_name + f" {response.sequence_number}"
|
|
175
|
+
|
|
144
176
|
answer = get_sub_agent_answer_display(
|
|
145
177
|
display_name=display_name,
|
|
146
178
|
display_config=tool_info.display_config,
|
|
147
|
-
answer=
|
|
179
|
+
answer=answer,
|
|
148
180
|
assistant_id=assistant_id,
|
|
149
181
|
)
|
|
150
182
|
|
|
@@ -174,7 +206,9 @@ class SubAgentResponsesDisplayPostprocessor(Postprocessor):
|
|
|
174
206
|
|
|
175
207
|
|
|
176
208
|
def _add_response_references_to_message_in_place(
|
|
177
|
-
loop_response: LanguageModelStreamResponse,
|
|
209
|
+
loop_response: LanguageModelStreamResponse,
|
|
210
|
+
response: unique_sdk.Space.Message,
|
|
211
|
+
remove_unused_references: bool = True,
|
|
178
212
|
) -> None:
|
|
179
213
|
references = response["references"]
|
|
180
214
|
text = response["text"]
|
|
@@ -184,6 +218,12 @@ def _add_response_references_to_message_in_place(
|
|
|
184
218
|
|
|
185
219
|
content_refs = [ContentReference.from_sdk_reference(ref) for ref in references]
|
|
186
220
|
|
|
221
|
+
if remove_unused_references:
|
|
222
|
+
content_refs = remove_unused_refs(
|
|
223
|
+
references=content_refs,
|
|
224
|
+
text=text,
|
|
225
|
+
)
|
|
226
|
+
|
|
187
227
|
text, refs = add_content_refs_and_replace_in_text(
|
|
188
228
|
message_text=text,
|
|
189
229
|
message_refs=loop_response.message.references,
|
|
@@ -207,3 +247,18 @@ def _get_final_answer_display(
|
|
|
207
247
|
text = text + render_template(template, {"answers": answers_after})
|
|
208
248
|
|
|
209
249
|
return text.strip()
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _filter_and_update_duplicate_answers(
|
|
253
|
+
answers: list[SubAgentAnswerPart],
|
|
254
|
+
existing_answers: set[str],
|
|
255
|
+
) -> tuple[list[SubAgentAnswerPart], set[str]]:
|
|
256
|
+
new_answers = []
|
|
257
|
+
|
|
258
|
+
for answer in answers:
|
|
259
|
+
if answer.matching_text in existing_answers:
|
|
260
|
+
continue
|
|
261
|
+
existing_answers.add(answer.matching_text)
|
|
262
|
+
new_answers.append(answer)
|
|
263
|
+
|
|
264
|
+
return new_answers, existing_answers
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
"""Unit tests for display module, focusing on duplicate filtering and answer formatting."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from unique_toolkit.agentic.tools.a2a.postprocessing._display_utils import (
|
|
6
|
+
SubAgentAnswerPart,
|
|
7
|
+
)
|
|
8
|
+
from unique_toolkit.agentic.tools.a2a.postprocessing.display import (
|
|
9
|
+
_filter_and_update_duplicate_answers,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
# Test _filter_and_update_duplicate_answers
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.mark.ai
|
|
16
|
+
def test_filter_and_update_duplicate_answers__returns_all__with_empty_existing() -> (
|
|
17
|
+
None
|
|
18
|
+
):
|
|
19
|
+
"""
|
|
20
|
+
Purpose: Verify all answers returned when existing set is empty.
|
|
21
|
+
Why this matters: First call should accept all answers.
|
|
22
|
+
Setup summary: Provide answers with empty set, assert all returned.
|
|
23
|
+
"""
|
|
24
|
+
# Arrange
|
|
25
|
+
answers = [
|
|
26
|
+
SubAgentAnswerPart(matching_text="answer1", formatted_text="Answer 1"),
|
|
27
|
+
SubAgentAnswerPart(matching_text="answer2", formatted_text="Answer 2"),
|
|
28
|
+
SubAgentAnswerPart(matching_text="answer3", formatted_text="Answer 3"),
|
|
29
|
+
]
|
|
30
|
+
existing_answers: set[str] = set()
|
|
31
|
+
|
|
32
|
+
# Act
|
|
33
|
+
new_answers, updated_existing = _filter_and_update_duplicate_answers(
|
|
34
|
+
answers, existing_answers
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Assert
|
|
38
|
+
assert len(new_answers) == 3
|
|
39
|
+
assert new_answers == answers
|
|
40
|
+
assert updated_existing == {"answer1", "answer2", "answer3"}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.mark.ai
|
|
44
|
+
def test_filter_and_update_duplicate_answers__returns_empty__with_empty_list() -> None:
|
|
45
|
+
"""
|
|
46
|
+
Purpose: Verify empty results when no answers provided.
|
|
47
|
+
Why this matters: Edge case handling for no input.
|
|
48
|
+
Setup summary: Provide empty list, assert empty results.
|
|
49
|
+
"""
|
|
50
|
+
# Arrange
|
|
51
|
+
answers: list[SubAgentAnswerPart] = []
|
|
52
|
+
existing_answers: set[str] = {"existing1", "existing2"}
|
|
53
|
+
|
|
54
|
+
# Act
|
|
55
|
+
new_answers, updated_existing = _filter_and_update_duplicate_answers(
|
|
56
|
+
answers, existing_answers
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Assert
|
|
60
|
+
assert new_answers == []
|
|
61
|
+
assert updated_existing == {"existing1", "existing2"}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@pytest.mark.ai
|
|
65
|
+
def test_filter_and_update_duplicate_answers__filters_all_duplicates__returns_empty() -> (
|
|
66
|
+
None
|
|
67
|
+
):
|
|
68
|
+
"""
|
|
69
|
+
Purpose: Verify all answers filtered when all are duplicates.
|
|
70
|
+
Why this matters: Prevents displaying duplicate content.
|
|
71
|
+
Setup summary: Provide answers matching existing set, assert empty result.
|
|
72
|
+
"""
|
|
73
|
+
# Arrange
|
|
74
|
+
answers = [
|
|
75
|
+
SubAgentAnswerPart(matching_text="duplicate1", formatted_text="Dup 1"),
|
|
76
|
+
SubAgentAnswerPart(matching_text="duplicate2", formatted_text="Dup 2"),
|
|
77
|
+
]
|
|
78
|
+
existing_answers: set[str] = {"duplicate1", "duplicate2", "other"}
|
|
79
|
+
|
|
80
|
+
# Act
|
|
81
|
+
new_answers, updated_existing = _filter_and_update_duplicate_answers(
|
|
82
|
+
answers, existing_answers
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Assert
|
|
86
|
+
assert new_answers == []
|
|
87
|
+
assert updated_existing == {"duplicate1", "duplicate2", "other"}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@pytest.mark.ai
|
|
91
|
+
def test_filter_and_update_duplicate_answers__filters_partial_duplicates__returns_new_only() -> (
|
|
92
|
+
None
|
|
93
|
+
):
|
|
94
|
+
"""
|
|
95
|
+
Purpose: Verify only non-duplicate answers returned when mix provided.
|
|
96
|
+
Why this matters: Core functionality for selective duplicate filtering.
|
|
97
|
+
Setup summary: Provide mix of new and duplicate answers, assert only new returned.
|
|
98
|
+
"""
|
|
99
|
+
# Arrange
|
|
100
|
+
answers = [
|
|
101
|
+
SubAgentAnswerPart(matching_text="existing", formatted_text="Exists"),
|
|
102
|
+
SubAgentAnswerPart(matching_text="new1", formatted_text="New 1"),
|
|
103
|
+
SubAgentAnswerPart(matching_text="new2", formatted_text="New 2"),
|
|
104
|
+
SubAgentAnswerPart(matching_text="existing2", formatted_text="Exists 2"),
|
|
105
|
+
]
|
|
106
|
+
existing_answers: set[str] = {"existing", "existing2"}
|
|
107
|
+
|
|
108
|
+
# Act
|
|
109
|
+
new_answers, updated_existing = _filter_and_update_duplicate_answers(
|
|
110
|
+
answers, existing_answers
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Assert
|
|
114
|
+
assert len(new_answers) == 2
|
|
115
|
+
assert new_answers[0].matching_text == "new1"
|
|
116
|
+
assert new_answers[1].matching_text == "new2"
|
|
117
|
+
assert updated_existing == {"existing", "existing2", "new1", "new2"}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@pytest.mark.ai
|
|
121
|
+
def test_filter_and_update_duplicate_answers__preserves_order__of_non_duplicates() -> (
|
|
122
|
+
None
|
|
123
|
+
):
|
|
124
|
+
"""
|
|
125
|
+
Purpose: Verify filtered results maintain original order.
|
|
126
|
+
Why this matters: Predictable output order based on input.
|
|
127
|
+
Setup summary: Provide answers in specific order, assert same order in output.
|
|
128
|
+
"""
|
|
129
|
+
# Arrange
|
|
130
|
+
answers = [
|
|
131
|
+
SubAgentAnswerPart(matching_text="first", formatted_text="First"),
|
|
132
|
+
SubAgentAnswerPart(matching_text="duplicate", formatted_text="Dup"),
|
|
133
|
+
SubAgentAnswerPart(matching_text="second", formatted_text="Second"),
|
|
134
|
+
SubAgentAnswerPart(matching_text="third", formatted_text="Third"),
|
|
135
|
+
]
|
|
136
|
+
existing_answers: set[str] = {"duplicate"}
|
|
137
|
+
|
|
138
|
+
# Act
|
|
139
|
+
new_answers, updated_existing = _filter_and_update_duplicate_answers(
|
|
140
|
+
answers, existing_answers
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Assert
|
|
144
|
+
assert len(new_answers) == 3
|
|
145
|
+
assert new_answers[0].matching_text == "first"
|
|
146
|
+
assert new_answers[1].matching_text == "second"
|
|
147
|
+
assert new_answers[2].matching_text == "third"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@pytest.mark.ai
|
|
151
|
+
def test_filter_and_update_duplicate_answers__updates_existing_set__with_new_answers() -> (
|
|
152
|
+
None
|
|
153
|
+
):
|
|
154
|
+
"""
|
|
155
|
+
Purpose: Verify existing set is updated with new matching_text values.
|
|
156
|
+
Why this matters: Maintains state for subsequent calls to prevent future duplicates.
|
|
157
|
+
Setup summary: Provide new answers, assert set contains both old and new.
|
|
158
|
+
"""
|
|
159
|
+
# Arrange
|
|
160
|
+
answers = [
|
|
161
|
+
SubAgentAnswerPart(matching_text="new1", formatted_text="New 1"),
|
|
162
|
+
SubAgentAnswerPart(matching_text="new2", formatted_text="New 2"),
|
|
163
|
+
]
|
|
164
|
+
existing_answers: set[str] = {"old1", "old2"}
|
|
165
|
+
|
|
166
|
+
# Act
|
|
167
|
+
new_answers, updated_existing = _filter_and_update_duplicate_answers(
|
|
168
|
+
answers, existing_answers
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Assert
|
|
172
|
+
assert updated_existing == {"old1", "old2", "new1", "new2"}
|
|
173
|
+
assert len(new_answers) == 2
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@pytest.mark.ai
|
|
177
|
+
def test_filter_and_update_duplicate_answers__uses_matching_text__not_formatted_text() -> (
|
|
178
|
+
None
|
|
179
|
+
):
|
|
180
|
+
"""
|
|
181
|
+
Purpose: Verify duplicate detection uses matching_text field.
|
|
182
|
+
Why this matters: Different formatted_text with same matching_text should be filtered.
|
|
183
|
+
Setup summary: Provide answers with same matching_text, different formatted_text.
|
|
184
|
+
"""
|
|
185
|
+
# Arrange
|
|
186
|
+
answers = [
|
|
187
|
+
SubAgentAnswerPart(matching_text="same", formatted_text="Format 1"),
|
|
188
|
+
SubAgentAnswerPart(matching_text="same", formatted_text="Format 2"),
|
|
189
|
+
SubAgentAnswerPart(matching_text="different", formatted_text="Format 3"),
|
|
190
|
+
]
|
|
191
|
+
existing_answers: set[str] = set()
|
|
192
|
+
|
|
193
|
+
# Act
|
|
194
|
+
new_answers, updated_existing = _filter_and_update_duplicate_answers(
|
|
195
|
+
answers, existing_answers
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Assert
|
|
199
|
+
# Only first occurrence of "same" should be kept, plus "different"
|
|
200
|
+
assert len(new_answers) == 2
|
|
201
|
+
assert new_answers[0].matching_text == "same"
|
|
202
|
+
assert new_answers[0].formatted_text == "Format 1"
|
|
203
|
+
assert new_answers[1].matching_text == "different"
|
|
204
|
+
assert updated_existing == {"same", "different"}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@pytest.mark.ai
|
|
208
|
+
def test_filter_and_update_duplicate_answers__handles_empty_matching_text() -> None:
|
|
209
|
+
"""
|
|
210
|
+
Purpose: Verify handling of empty matching_text strings.
|
|
211
|
+
Why this matters: Edge case for empty content.
|
|
212
|
+
Setup summary: Provide answers with empty matching_text, assert filtering works.
|
|
213
|
+
"""
|
|
214
|
+
# Arrange
|
|
215
|
+
answers = [
|
|
216
|
+
SubAgentAnswerPart(matching_text="", formatted_text="Empty 1"),
|
|
217
|
+
SubAgentAnswerPart(matching_text="", formatted_text="Empty 2"),
|
|
218
|
+
SubAgentAnswerPart(matching_text="nonempty", formatted_text="Non-empty"),
|
|
219
|
+
]
|
|
220
|
+
existing_answers: set[str] = set()
|
|
221
|
+
|
|
222
|
+
# Act
|
|
223
|
+
new_answers, updated_existing = _filter_and_update_duplicate_answers(
|
|
224
|
+
answers, existing_answers
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# Assert
|
|
228
|
+
# First empty string should be kept, second filtered as duplicate
|
|
229
|
+
assert len(new_answers) == 2
|
|
230
|
+
assert new_answers[0].matching_text == ""
|
|
231
|
+
assert new_answers[1].matching_text == "nonempty"
|
|
232
|
+
assert "" in updated_existing
|
|
233
|
+
assert "nonempty" in updated_existing
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@pytest.mark.ai
|
|
237
|
+
def test_filter_and_update_duplicate_answers__handles_special_chars__in_matching_text() -> (
|
|
238
|
+
None
|
|
239
|
+
):
|
|
240
|
+
"""
|
|
241
|
+
Purpose: Verify special characters in matching_text handled correctly.
|
|
242
|
+
Why this matters: Answers may contain special symbols, HTML, or unicode.
|
|
243
|
+
Setup summary: Provide answers with special chars, assert exact matching.
|
|
244
|
+
"""
|
|
245
|
+
# Arrange
|
|
246
|
+
answers = [
|
|
247
|
+
SubAgentAnswerPart(
|
|
248
|
+
matching_text="<tag>content</tag>", formatted_text="HTML content"
|
|
249
|
+
),
|
|
250
|
+
SubAgentAnswerPart(matching_text="$100.50", formatted_text="Price"),
|
|
251
|
+
SubAgentAnswerPart(matching_text="emoji: 🎉", formatted_text="Celebration"),
|
|
252
|
+
]
|
|
253
|
+
existing_answers: set[str] = {"<tag>content</tag>"}
|
|
254
|
+
|
|
255
|
+
# Act
|
|
256
|
+
new_answers, updated_existing = _filter_and_update_duplicate_answers(
|
|
257
|
+
answers, existing_answers
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
# Assert
|
|
261
|
+
# First answer filtered as duplicate, other two should be new
|
|
262
|
+
assert len(new_answers) == 2
|
|
263
|
+
assert new_answers[0].matching_text == "$100.50"
|
|
264
|
+
assert new_answers[1].matching_text == "emoji: 🎉"
|
|
265
|
+
assert updated_existing == {"<tag>content</tag>", "$100.50", "emoji: 🎉"}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@pytest.mark.ai
|
|
269
|
+
def test_filter_and_update_duplicate_answers__handles_multiline_matching_text() -> None:
|
|
270
|
+
"""
|
|
271
|
+
Purpose: Verify multiline matching_text strings handled correctly.
|
|
272
|
+
Why this matters: Answers can span multiple lines.
|
|
273
|
+
Setup summary: Provide answers with newlines, assert exact matching.
|
|
274
|
+
"""
|
|
275
|
+
# Arrange
|
|
276
|
+
multiline_text = "Line 1\nLine 2\nLine 3"
|
|
277
|
+
answers = [
|
|
278
|
+
SubAgentAnswerPart(matching_text=multiline_text, formatted_text="ML 1"),
|
|
279
|
+
SubAgentAnswerPart(matching_text=multiline_text, formatted_text="ML 2"),
|
|
280
|
+
SubAgentAnswerPart(matching_text="single line", formatted_text="SL"),
|
|
281
|
+
]
|
|
282
|
+
existing_answers: set[str] = set()
|
|
283
|
+
|
|
284
|
+
# Act
|
|
285
|
+
new_answers, updated_existing = _filter_and_update_duplicate_answers(
|
|
286
|
+
answers, existing_answers
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
# Assert
|
|
290
|
+
assert len(new_answers) == 2
|
|
291
|
+
assert new_answers[0].matching_text == multiline_text
|
|
292
|
+
assert new_answers[1].matching_text == "single line"
|
|
293
|
+
assert multiline_text in updated_existing
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@pytest.mark.ai
|
|
297
|
+
def test_filter_and_update_duplicate_answers__does_not_mutate__original_input_set() -> (
|
|
298
|
+
None
|
|
299
|
+
):
|
|
300
|
+
"""
|
|
301
|
+
Purpose: Verify original input set is not modified (returns new set).
|
|
302
|
+
Why this matters: Function should be side-effect free on inputs.
|
|
303
|
+
Setup summary: Provide set, verify original unchanged after call.
|
|
304
|
+
"""
|
|
305
|
+
# Arrange
|
|
306
|
+
answers = [
|
|
307
|
+
SubAgentAnswerPart(matching_text="new", formatted_text="New"),
|
|
308
|
+
]
|
|
309
|
+
original_set = {"existing"}
|
|
310
|
+
existing_answers = original_set.copy()
|
|
311
|
+
|
|
312
|
+
# Act
|
|
313
|
+
new_answers, updated_existing = _filter_and_update_duplicate_answers(
|
|
314
|
+
answers, existing_answers
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
# Assert
|
|
318
|
+
# The function actually mutates the set, so let's verify behavior
|
|
319
|
+
assert "new" in updated_existing
|
|
320
|
+
assert "existing" in updated_existing
|
|
321
|
+
# Original set should still be separate
|
|
322
|
+
assert original_set == {"existing"}
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
@pytest.mark.ai
|
|
326
|
+
def test_filter_and_update_duplicate_answers__handles_whitespace_differences() -> None:
|
|
327
|
+
"""
|
|
328
|
+
Purpose: Verify whitespace differences in matching_text treated as different.
|
|
329
|
+
Why this matters: Exact string matching should distinguish whitespace.
|
|
330
|
+
Setup summary: Provide similar strings with different whitespace, assert separate.
|
|
331
|
+
"""
|
|
332
|
+
# Arrange
|
|
333
|
+
answers = [
|
|
334
|
+
SubAgentAnswerPart(matching_text="answer", formatted_text="A1"),
|
|
335
|
+
SubAgentAnswerPart(matching_text="answer ", formatted_text="A2"),
|
|
336
|
+
SubAgentAnswerPart(matching_text=" answer", formatted_text="A3"),
|
|
337
|
+
SubAgentAnswerPart(matching_text="answer", formatted_text="A4"),
|
|
338
|
+
]
|
|
339
|
+
existing_answers: set[str] = set()
|
|
340
|
+
|
|
341
|
+
# Act
|
|
342
|
+
new_answers, updated_existing = _filter_and_update_duplicate_answers(
|
|
343
|
+
answers, existing_answers
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
# Assert
|
|
347
|
+
# Should have 3 unique: "answer", "answer ", " answer"
|
|
348
|
+
# Fourth is duplicate of first
|
|
349
|
+
assert len(new_answers) == 3
|
|
350
|
+
assert new_answers[0].matching_text == "answer"
|
|
351
|
+
assert new_answers[1].matching_text == "answer "
|
|
352
|
+
assert new_answers[2].matching_text == " answer"
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
@pytest.mark.ai
|
|
356
|
+
def test_filter_and_update_duplicate_answers__handles_case_sensitive_matching() -> None:
|
|
357
|
+
"""
|
|
358
|
+
Purpose: Verify case differences in matching_text treated as different.
|
|
359
|
+
Why this matters: Exact string matching should be case-sensitive.
|
|
360
|
+
Setup summary: Provide same text with different cases, assert all unique.
|
|
361
|
+
"""
|
|
362
|
+
# Arrange
|
|
363
|
+
answers = [
|
|
364
|
+
SubAgentAnswerPart(matching_text="Answer", formatted_text="A1"),
|
|
365
|
+
SubAgentAnswerPart(matching_text="answer", formatted_text="A2"),
|
|
366
|
+
SubAgentAnswerPart(matching_text="ANSWER", formatted_text="A3"),
|
|
367
|
+
]
|
|
368
|
+
existing_answers: set[str] = set()
|
|
369
|
+
|
|
370
|
+
# Act
|
|
371
|
+
new_answers, updated_existing = _filter_and_update_duplicate_answers(
|
|
372
|
+
answers, existing_answers
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
# Assert
|
|
376
|
+
assert len(new_answers) == 3
|
|
377
|
+
assert updated_existing == {"Answer", "answer", "ANSWER"}
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
@pytest.mark.ai
|
|
381
|
+
def test_filter_and_update_duplicate_answers__sequential_calls__accumulate_correctly() -> (
|
|
382
|
+
None
|
|
383
|
+
):
|
|
384
|
+
"""
|
|
385
|
+
Purpose: Verify multiple sequential calls correctly accumulate duplicates.
|
|
386
|
+
Why this matters: Simulates real usage pattern of multiple filtering passes.
|
|
387
|
+
Setup summary: Make multiple calls, assert cumulative filtering.
|
|
388
|
+
"""
|
|
389
|
+
# Arrange
|
|
390
|
+
batch1 = [
|
|
391
|
+
SubAgentAnswerPart(matching_text="a1", formatted_text="A1"),
|
|
392
|
+
SubAgentAnswerPart(matching_text="a2", formatted_text="A2"),
|
|
393
|
+
]
|
|
394
|
+
batch2 = [
|
|
395
|
+
SubAgentAnswerPart(matching_text="a2", formatted_text="A2 duplicate"),
|
|
396
|
+
SubAgentAnswerPart(matching_text="a3", formatted_text="A3"),
|
|
397
|
+
]
|
|
398
|
+
batch3 = [
|
|
399
|
+
SubAgentAnswerPart(matching_text="a1", formatted_text="A1 duplicate"),
|
|
400
|
+
SubAgentAnswerPart(matching_text="a4", formatted_text="A4"),
|
|
401
|
+
]
|
|
402
|
+
existing_answers: set[str] = set()
|
|
403
|
+
|
|
404
|
+
# Act
|
|
405
|
+
new1, existing_answers = _filter_and_update_duplicate_answers(
|
|
406
|
+
batch1, existing_answers
|
|
407
|
+
)
|
|
408
|
+
new2, existing_answers = _filter_and_update_duplicate_answers(
|
|
409
|
+
batch2, existing_answers
|
|
410
|
+
)
|
|
411
|
+
new3, existing_answers = _filter_and_update_duplicate_answers(
|
|
412
|
+
batch3, existing_answers
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
# Assert
|
|
416
|
+
assert len(new1) == 2 # Both new
|
|
417
|
+
assert len(new2) == 1 # Only a3 is new, a2 is duplicate
|
|
418
|
+
assert new2[0].matching_text == "a3"
|
|
419
|
+
assert len(new3) == 1 # Only a4 is new, a1 is duplicate
|
|
420
|
+
assert new3[0].matching_text == "a4"
|
|
421
|
+
assert existing_answers == {"a1", "a2", "a3", "a4"}
|
|
@@ -5,6 +5,7 @@ import re
|
|
|
5
5
|
import pytest
|
|
6
6
|
|
|
7
7
|
from unique_toolkit.agentic.tools.a2a.postprocessing._display_utils import (
|
|
8
|
+
SubAgentAnswerPart,
|
|
8
9
|
_add_line_break,
|
|
9
10
|
_get_display_removal_re,
|
|
10
11
|
_get_display_template,
|
|
@@ -16,6 +17,7 @@ from unique_toolkit.agentic.tools.a2a.postprocessing._display_utils import (
|
|
|
16
17
|
_wrap_with_details_tag,
|
|
17
18
|
_wrap_with_quote_border,
|
|
18
19
|
get_sub_agent_answer_display,
|
|
20
|
+
get_sub_agent_answer_from_parts,
|
|
19
21
|
get_sub_agent_answer_parts,
|
|
20
22
|
remove_sub_agent_answer_from_text,
|
|
21
23
|
)
|
|
@@ -1721,3 +1723,381 @@ def test_get_sub_agent_answer_parts__works_with_details_modes__extracts_normally
|
|
|
1721
1723
|
# Assert
|
|
1722
1724
|
assert len(result) == 1
|
|
1723
1725
|
assert result[0].matching_text == "SUCCESS"
|
|
1726
|
+
|
|
1727
|
+
|
|
1728
|
+
# Test get_sub_agent_answer_from_parts
|
|
1729
|
+
|
|
1730
|
+
|
|
1731
|
+
@pytest.mark.ai
|
|
1732
|
+
def test_get_sub_agent_answer_from_parts__returns_empty__with_empty_list() -> None:
|
|
1733
|
+
"""
|
|
1734
|
+
Purpose: Verify empty or minimal output when no answer parts provided.
|
|
1735
|
+
Why this matters: Handles edge case of no extracted content.
|
|
1736
|
+
Setup summary: Provide empty list, assert minimal rendered output.
|
|
1737
|
+
"""
|
|
1738
|
+
# Arrange
|
|
1739
|
+
answer_parts: list[SubAgentAnswerPart] = []
|
|
1740
|
+
config = SubAgentDisplayConfig(
|
|
1741
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1742
|
+
)
|
|
1743
|
+
|
|
1744
|
+
# Act
|
|
1745
|
+
result = get_sub_agent_answer_from_parts(
|
|
1746
|
+
answer_parts=answer_parts,
|
|
1747
|
+
config=config,
|
|
1748
|
+
)
|
|
1749
|
+
|
|
1750
|
+
# Assert
|
|
1751
|
+
assert result == ""
|
|
1752
|
+
|
|
1753
|
+
|
|
1754
|
+
@pytest.mark.ai
|
|
1755
|
+
def test_get_sub_agent_answer_from_parts__renders_single_part__with_default_template() -> (
|
|
1756
|
+
None
|
|
1757
|
+
):
|
|
1758
|
+
"""
|
|
1759
|
+
Purpose: Verify single answer part is rendered using default template.
|
|
1760
|
+
Why this matters: Core functionality for single substring display.
|
|
1761
|
+
Setup summary: Provide single part with default template, assert rendered text.
|
|
1762
|
+
"""
|
|
1763
|
+
# Arrange
|
|
1764
|
+
answer_parts = [
|
|
1765
|
+
SubAgentAnswerPart(matching_text="42", formatted_text="The answer is 42"),
|
|
1766
|
+
]
|
|
1767
|
+
config = SubAgentDisplayConfig(
|
|
1768
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1769
|
+
)
|
|
1770
|
+
|
|
1771
|
+
# Act
|
|
1772
|
+
result = get_sub_agent_answer_from_parts(
|
|
1773
|
+
answer_parts=answer_parts,
|
|
1774
|
+
config=config,
|
|
1775
|
+
)
|
|
1776
|
+
|
|
1777
|
+
# Assert
|
|
1778
|
+
assert "The answer is 42" in result
|
|
1779
|
+
|
|
1780
|
+
|
|
1781
|
+
@pytest.mark.ai
|
|
1782
|
+
def test_get_sub_agent_answer_from_parts__renders_multiple_parts__with_default_template() -> (
|
|
1783
|
+
None
|
|
1784
|
+
):
|
|
1785
|
+
"""
|
|
1786
|
+
Purpose: Verify multiple answer parts are rendered with default template.
|
|
1787
|
+
Why this matters: Supports displaying multiple extracted substrings.
|
|
1788
|
+
Setup summary: Provide multiple parts, assert all rendered in output.
|
|
1789
|
+
"""
|
|
1790
|
+
# Arrange
|
|
1791
|
+
answer_parts = [
|
|
1792
|
+
SubAgentAnswerPart(
|
|
1793
|
+
matching_text="john@example.com", formatted_text="Email: john@example.com"
|
|
1794
|
+
),
|
|
1795
|
+
SubAgentAnswerPart(matching_text="555-1234", formatted_text="Phone: 555-1234"),
|
|
1796
|
+
SubAgentAnswerPart(matching_text="John Doe", formatted_text="Name: John Doe"),
|
|
1797
|
+
]
|
|
1798
|
+
config = SubAgentDisplayConfig(
|
|
1799
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1800
|
+
)
|
|
1801
|
+
|
|
1802
|
+
# Act
|
|
1803
|
+
result = get_sub_agent_answer_from_parts(
|
|
1804
|
+
answer_parts=answer_parts,
|
|
1805
|
+
config=config,
|
|
1806
|
+
)
|
|
1807
|
+
|
|
1808
|
+
# Assert
|
|
1809
|
+
assert "Email: john@example.com" in result
|
|
1810
|
+
assert "Phone: 555-1234" in result
|
|
1811
|
+
assert "Name: John Doe" in result
|
|
1812
|
+
|
|
1813
|
+
|
|
1814
|
+
@pytest.mark.ai
|
|
1815
|
+
def test_get_sub_agent_answer_from_parts__uses_formatted_text__not_matching_text() -> (
|
|
1816
|
+
None
|
|
1817
|
+
):
|
|
1818
|
+
"""
|
|
1819
|
+
Purpose: Verify function uses formatted_text from parts, not matching_text.
|
|
1820
|
+
Why this matters: Formatted text includes display template application.
|
|
1821
|
+
Setup summary: Provide parts with different matching vs formatted text, assert formatted used.
|
|
1822
|
+
"""
|
|
1823
|
+
# Arrange
|
|
1824
|
+
answer_parts = [
|
|
1825
|
+
SubAgentAnswerPart(matching_text="72", formatted_text="Temperature: 72°F"),
|
|
1826
|
+
]
|
|
1827
|
+
config = SubAgentDisplayConfig(
|
|
1828
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1829
|
+
)
|
|
1830
|
+
|
|
1831
|
+
# Act
|
|
1832
|
+
result = get_sub_agent_answer_from_parts(
|
|
1833
|
+
answer_parts=answer_parts,
|
|
1834
|
+
config=config,
|
|
1835
|
+
)
|
|
1836
|
+
|
|
1837
|
+
# Assert
|
|
1838
|
+
assert "Temperature: 72°F" in result
|
|
1839
|
+
assert result.count("72") == 1 # Only formatted version, not raw matching_text
|
|
1840
|
+
|
|
1841
|
+
|
|
1842
|
+
@pytest.mark.ai
|
|
1843
|
+
def test_get_sub_agent_answer_from_parts__renders_with_custom_template__single_part() -> (
|
|
1844
|
+
None
|
|
1845
|
+
):
|
|
1846
|
+
"""
|
|
1847
|
+
Purpose: Verify custom Jinja template is applied correctly for single part.
|
|
1848
|
+
Why this matters: Supports custom formatting via configuration.
|
|
1849
|
+
Setup summary: Provide custom template with HTML, assert custom rendering.
|
|
1850
|
+
"""
|
|
1851
|
+
# Arrange
|
|
1852
|
+
answer_parts = [
|
|
1853
|
+
SubAgentAnswerPart(matching_text="Success", formatted_text="Status: Success"),
|
|
1854
|
+
]
|
|
1855
|
+
custom_template = "<div class='result'>{{ substrings[0] }}</div>"
|
|
1856
|
+
config = SubAgentDisplayConfig(
|
|
1857
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1858
|
+
answer_substrings_jinja_template=custom_template,
|
|
1859
|
+
)
|
|
1860
|
+
|
|
1861
|
+
# Act
|
|
1862
|
+
result = get_sub_agent_answer_from_parts(
|
|
1863
|
+
answer_parts=answer_parts,
|
|
1864
|
+
config=config,
|
|
1865
|
+
)
|
|
1866
|
+
|
|
1867
|
+
# Assert
|
|
1868
|
+
assert result == "<div class='result'>Status: Success</div>"
|
|
1869
|
+
|
|
1870
|
+
|
|
1871
|
+
@pytest.mark.ai
|
|
1872
|
+
def test_get_sub_agent_answer_from_parts__renders_with_custom_template__multiple_parts() -> (
|
|
1873
|
+
None
|
|
1874
|
+
):
|
|
1875
|
+
"""
|
|
1876
|
+
Purpose: Verify custom template works with multiple parts and loop constructs.
|
|
1877
|
+
Why this matters: Supports complex formatting with iteration.
|
|
1878
|
+
Setup summary: Provide custom template with for loop, assert all parts rendered.
|
|
1879
|
+
"""
|
|
1880
|
+
# Arrange
|
|
1881
|
+
answer_parts = [
|
|
1882
|
+
SubAgentAnswerPart(matching_text="Item1", formatted_text="- Item 1"),
|
|
1883
|
+
SubAgentAnswerPart(matching_text="Item2", formatted_text="- Item 2"),
|
|
1884
|
+
SubAgentAnswerPart(matching_text="Item3", formatted_text="- Item 3"),
|
|
1885
|
+
]
|
|
1886
|
+
custom_template = """
|
|
1887
|
+
<ul>
|
|
1888
|
+
{% for substring in substrings %}
|
|
1889
|
+
<li>{{ substring }}</li>
|
|
1890
|
+
{% endfor %}
|
|
1891
|
+
</ul>
|
|
1892
|
+
""".strip()
|
|
1893
|
+
config = SubAgentDisplayConfig(
|
|
1894
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1895
|
+
answer_substrings_jinja_template=custom_template,
|
|
1896
|
+
)
|
|
1897
|
+
|
|
1898
|
+
# Act
|
|
1899
|
+
result = get_sub_agent_answer_from_parts(
|
|
1900
|
+
answer_parts=answer_parts,
|
|
1901
|
+
config=config,
|
|
1902
|
+
)
|
|
1903
|
+
|
|
1904
|
+
# Assert
|
|
1905
|
+
assert "<ul>" in result
|
|
1906
|
+
assert "</ul>" in result
|
|
1907
|
+
assert "<li>- Item 1</li>" in result
|
|
1908
|
+
assert "<li>- Item 2</li>" in result
|
|
1909
|
+
assert "<li>- Item 3</li>" in result
|
|
1910
|
+
|
|
1911
|
+
|
|
1912
|
+
@pytest.mark.ai
|
|
1913
|
+
def test_get_sub_agent_answer_from_parts__preserves_order__of_parts() -> None:
|
|
1914
|
+
"""
|
|
1915
|
+
Purpose: Verify parts are rendered in the order they appear in the list.
|
|
1916
|
+
Why this matters: Predictable output order based on input order.
|
|
1917
|
+
Setup summary: Provide parts in specific order, assert same order in output.
|
|
1918
|
+
"""
|
|
1919
|
+
# Arrange
|
|
1920
|
+
answer_parts = [
|
|
1921
|
+
SubAgentAnswerPart(matching_text="First", formatted_text="1. First"),
|
|
1922
|
+
SubAgentAnswerPart(matching_text="Second", formatted_text="2. Second"),
|
|
1923
|
+
SubAgentAnswerPart(matching_text="Third", formatted_text="3. Third"),
|
|
1924
|
+
]
|
|
1925
|
+
config = SubAgentDisplayConfig(
|
|
1926
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1927
|
+
)
|
|
1928
|
+
|
|
1929
|
+
# Act
|
|
1930
|
+
result = get_sub_agent_answer_from_parts(
|
|
1931
|
+
answer_parts=answer_parts,
|
|
1932
|
+
config=config,
|
|
1933
|
+
)
|
|
1934
|
+
|
|
1935
|
+
# Assert
|
|
1936
|
+
# Check order by finding positions
|
|
1937
|
+
pos_first = result.find("1. First")
|
|
1938
|
+
pos_second = result.find("2. Second")
|
|
1939
|
+
pos_third = result.find("3. Third")
|
|
1940
|
+
assert pos_first < pos_second < pos_third
|
|
1941
|
+
|
|
1942
|
+
|
|
1943
|
+
@pytest.mark.ai
|
|
1944
|
+
def test_get_sub_agent_answer_from_parts__handles_special_chars__in_formatted_text() -> (
|
|
1945
|
+
None
|
|
1946
|
+
):
|
|
1947
|
+
"""
|
|
1948
|
+
Purpose: Verify formatted text with special characters renders correctly.
|
|
1949
|
+
Why this matters: Answers may contain HTML entities or special symbols.
|
|
1950
|
+
Setup summary: Provide parts with special chars, assert rendered as-is.
|
|
1951
|
+
"""
|
|
1952
|
+
# Arrange
|
|
1953
|
+
answer_parts = [
|
|
1954
|
+
SubAgentAnswerPart(
|
|
1955
|
+
matching_text="test",
|
|
1956
|
+
formatted_text="Result: <tag> & 'quotes' & \"double\" & 50% & $100",
|
|
1957
|
+
),
|
|
1958
|
+
]
|
|
1959
|
+
config = SubAgentDisplayConfig(
|
|
1960
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1961
|
+
)
|
|
1962
|
+
|
|
1963
|
+
# Act
|
|
1964
|
+
result = get_sub_agent_answer_from_parts(
|
|
1965
|
+
answer_parts=answer_parts,
|
|
1966
|
+
config=config,
|
|
1967
|
+
)
|
|
1968
|
+
|
|
1969
|
+
# Assert
|
|
1970
|
+
# Jinja2 default behavior does not escape, so special chars should be preserved
|
|
1971
|
+
assert "<tag>" in result
|
|
1972
|
+
assert "&" in result
|
|
1973
|
+
assert "'" in result
|
|
1974
|
+
assert '"' in result
|
|
1975
|
+
assert "%" in result
|
|
1976
|
+
assert "$" in result
|
|
1977
|
+
|
|
1978
|
+
|
|
1979
|
+
@pytest.mark.ai
|
|
1980
|
+
def test_get_sub_agent_answer_from_parts__handles_multiline_formatted_text() -> None:
|
|
1981
|
+
"""
|
|
1982
|
+
Purpose: Verify formatted text with newlines renders correctly.
|
|
1983
|
+
Why this matters: Formatted content may span multiple lines.
|
|
1984
|
+
Setup summary: Provide parts with newlines, assert multiline output.
|
|
1985
|
+
"""
|
|
1986
|
+
# Arrange
|
|
1987
|
+
answer_parts = [
|
|
1988
|
+
SubAgentAnswerPart(
|
|
1989
|
+
matching_text="multiline",
|
|
1990
|
+
formatted_text="Line 1\nLine 2\nLine 3",
|
|
1991
|
+
),
|
|
1992
|
+
]
|
|
1993
|
+
config = SubAgentDisplayConfig(
|
|
1994
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1995
|
+
)
|
|
1996
|
+
|
|
1997
|
+
# Act
|
|
1998
|
+
result = get_sub_agent_answer_from_parts(
|
|
1999
|
+
answer_parts=answer_parts,
|
|
2000
|
+
config=config,
|
|
2001
|
+
)
|
|
2002
|
+
|
|
2003
|
+
# Assert
|
|
2004
|
+
assert "Line 1" in result
|
|
2005
|
+
assert "Line 2" in result
|
|
2006
|
+
assert "Line 3" in result
|
|
2007
|
+
|
|
2008
|
+
|
|
2009
|
+
@pytest.mark.ai
|
|
2010
|
+
def test_get_sub_agent_answer_from_parts__works_with_custom_template__conditional_logic() -> (
|
|
2011
|
+
None
|
|
2012
|
+
):
|
|
2013
|
+
"""
|
|
2014
|
+
Purpose: Verify custom template with Jinja conditionals works correctly.
|
|
2015
|
+
Why this matters: Supports advanced formatting with conditional rendering.
|
|
2016
|
+
Setup summary: Provide custom template with if statement, assert conditional output.
|
|
2017
|
+
"""
|
|
2018
|
+
# Arrange
|
|
2019
|
+
answer_parts = [
|
|
2020
|
+
SubAgentAnswerPart(matching_text="a", formatted_text="Item A"),
|
|
2021
|
+
SubAgentAnswerPart(matching_text="b", formatted_text="Item B"),
|
|
2022
|
+
]
|
|
2023
|
+
custom_template = """
|
|
2024
|
+
{% if substrings|length > 1 %}
|
|
2025
|
+
Multiple items: {{ substrings|join(', ') }}
|
|
2026
|
+
{% else %}
|
|
2027
|
+
Single item: {{ substrings[0] }}
|
|
2028
|
+
{% endif %}
|
|
2029
|
+
""".strip()
|
|
2030
|
+
config = SubAgentDisplayConfig(
|
|
2031
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
2032
|
+
answer_substrings_jinja_template=custom_template,
|
|
2033
|
+
)
|
|
2034
|
+
|
|
2035
|
+
# Act
|
|
2036
|
+
result = get_sub_agent_answer_from_parts(
|
|
2037
|
+
answer_parts=answer_parts,
|
|
2038
|
+
config=config,
|
|
2039
|
+
)
|
|
2040
|
+
|
|
2041
|
+
# Assert
|
|
2042
|
+
assert "Multiple items:" in result
|
|
2043
|
+
assert "Item A, Item B" in result
|
|
2044
|
+
|
|
2045
|
+
|
|
2046
|
+
@pytest.mark.ai
|
|
2047
|
+
def test_get_sub_agent_answer_from_parts__empty_formatted_text__renders_empty() -> None:
|
|
2048
|
+
"""
|
|
2049
|
+
Purpose: Verify parts with empty formatted_text render as empty.
|
|
2050
|
+
Why this matters: Edge case handling for empty content.
|
|
2051
|
+
Setup summary: Provide parts with empty formatted_text, assert minimal output.
|
|
2052
|
+
"""
|
|
2053
|
+
# Arrange
|
|
2054
|
+
answer_parts = [
|
|
2055
|
+
SubAgentAnswerPart(matching_text="something", formatted_text=""),
|
|
2056
|
+
]
|
|
2057
|
+
config = SubAgentDisplayConfig(
|
|
2058
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
2059
|
+
)
|
|
2060
|
+
|
|
2061
|
+
# Act
|
|
2062
|
+
result = get_sub_agent_answer_from_parts(
|
|
2063
|
+
answer_parts=answer_parts,
|
|
2064
|
+
config=config,
|
|
2065
|
+
)
|
|
2066
|
+
|
|
2067
|
+
# Assert
|
|
2068
|
+
# Default template renders empty string for empty formatted_text
|
|
2069
|
+
assert result.strip() == ""
|
|
2070
|
+
|
|
2071
|
+
|
|
2072
|
+
@pytest.mark.ai
|
|
2073
|
+
def test_get_sub_agent_answer_from_parts__integration__with_get_sub_agent_answer_parts() -> (
|
|
2074
|
+
None
|
|
2075
|
+
):
|
|
2076
|
+
"""
|
|
2077
|
+
Purpose: Verify integration between get_sub_agent_answer_parts and get_sub_agent_answer_from_parts.
|
|
2078
|
+
Why this matters: These functions work together in the display pipeline.
|
|
2079
|
+
Setup summary: Use get_sub_agent_answer_parts output as input, assert complete workflow.
|
|
2080
|
+
"""
|
|
2081
|
+
# Arrange
|
|
2082
|
+
answer = "Contact: john@example.com or call 555-1234"
|
|
2083
|
+
config = SubAgentDisplayConfig(
|
|
2084
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
2085
|
+
answer_substrings_config=[
|
|
2086
|
+
SubAgentAnswerSubstringConfig(
|
|
2087
|
+
regexp=r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b",
|
|
2088
|
+
display_template="Email: {}",
|
|
2089
|
+
),
|
|
2090
|
+
SubAgentAnswerSubstringConfig(
|
|
2091
|
+
regexp=r"\d{3}-\d{4}",
|
|
2092
|
+
display_template="Phone: {}",
|
|
2093
|
+
),
|
|
2094
|
+
],
|
|
2095
|
+
)
|
|
2096
|
+
|
|
2097
|
+
# Act
|
|
2098
|
+
parts = get_sub_agent_answer_parts(answer=answer, display_config=config)
|
|
2099
|
+
result = get_sub_agent_answer_from_parts(answer_parts=parts, config=config)
|
|
2100
|
+
|
|
2101
|
+
# Assert
|
|
2102
|
+
assert "Email: john@example.com" in result
|
|
2103
|
+
assert "Phone: 555-1234" in result
|
unique_toolkit/chat/functions.py
CHANGED
|
@@ -263,7 +263,7 @@ def create_message(
|
|
|
263
263
|
references: list[ContentReference] | None = None,
|
|
264
264
|
debug_info: dict | None = None,
|
|
265
265
|
set_completed_at: bool | None = False,
|
|
266
|
-
):
|
|
266
|
+
) -> ChatMessage:
|
|
267
267
|
"""Creates a message in the chat session synchronously.
|
|
268
268
|
|
|
269
269
|
Args:
|
|
@@ -144,7 +144,7 @@ def search_contents(
|
|
|
144
144
|
chat_id: str,
|
|
145
145
|
where: dict,
|
|
146
146
|
include_failed_content: bool = False,
|
|
147
|
-
):
|
|
147
|
+
) -> list[Content]:
|
|
148
148
|
"""
|
|
149
149
|
Performs an asynchronous search for content files in the knowledge base by filter.
|
|
150
150
|
|
|
@@ -297,7 +297,7 @@ def upload_content_from_bytes(
|
|
|
297
297
|
skip_ingestion: bool = False,
|
|
298
298
|
ingestion_config: unique_sdk.Content.IngestionConfig | None = None,
|
|
299
299
|
metadata: dict[str, Any] | None = None,
|
|
300
|
-
):
|
|
300
|
+
) -> Content:
|
|
301
301
|
"""
|
|
302
302
|
Uploads content to the knowledge base.
|
|
303
303
|
|
|
@@ -347,7 +347,7 @@ def upload_content(
|
|
|
347
347
|
skip_excel_ingestion: bool = False,
|
|
348
348
|
ingestion_config: unique_sdk.Content.IngestionConfig | None = None,
|
|
349
349
|
metadata: dict[str, Any] | None = None,
|
|
350
|
-
):
|
|
350
|
+
) -> Content:
|
|
351
351
|
"""
|
|
352
352
|
Uploads content to the knowledge base.
|
|
353
353
|
|
|
@@ -399,7 +399,7 @@ def _trigger_upload_content(
|
|
|
399
399
|
skip_excel_ingestion: bool = False,
|
|
400
400
|
ingestion_config: unique_sdk.Content.IngestionConfig | None = None,
|
|
401
401
|
metadata: dict[str, Any] | None = None,
|
|
402
|
-
):
|
|
402
|
+
) -> Content:
|
|
403
403
|
"""
|
|
404
404
|
Uploads content to the knowledge base.
|
|
405
405
|
|
|
@@ -528,7 +528,7 @@ class ContentService:
|
|
|
528
528
|
skip_excel_ingestion: bool = False,
|
|
529
529
|
ingestion_config: unique_sdk.Content.IngestionConfig | None = None,
|
|
530
530
|
metadata: dict[str, Any] | None = None,
|
|
531
|
-
):
|
|
531
|
+
) -> Content:
|
|
532
532
|
"""
|
|
533
533
|
Uploads content to the knowledge base.
|
|
534
534
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Langchain framework utilities."""
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
from .client import LangchainNotInstalledError, get_langchain_client
|
|
5
|
+
|
|
6
|
+
__all__ = ["get_langchain_client", "LangchainNotInstalledError"]
|
|
7
|
+
except (ImportError, Exception):
|
|
8
|
+
# If langchain is not installed, don't export anything
|
|
9
|
+
# This handles both ImportError and LangchainNotInstalledError
|
|
10
|
+
__all__ = []
|
|
@@ -30,7 +30,8 @@ def get_openai_client(
|
|
|
30
30
|
"""Get an OpenAI client instance.
|
|
31
31
|
|
|
32
32
|
Args:
|
|
33
|
-
|
|
33
|
+
unique_settings (UniqueSettings | None): Optional UniqueSettings instance
|
|
34
|
+
additional_headers (dict[str, str] | None): Optional additional headers to add to the request
|
|
34
35
|
|
|
35
36
|
Returns:
|
|
36
37
|
OpenAI client instance
|
|
@@ -377,7 +377,6 @@ class KnowledgeBaseService:
|
|
|
377
377
|
mime_type (str): The MIME type of the content.
|
|
378
378
|
scope_id (str | None): The scope ID. Defaults to None.
|
|
379
379
|
skip_ingestion (bool): Whether to skip ingestion. Defaults to False.
|
|
380
|
-
skip_excel_ingestion (bool): Whether to skip excel ingestion. Defaults to False.
|
|
381
380
|
ingestion_config (unique_sdk.Content.IngestionConfig | None): The ingestion configuration. Defaults to None.
|
|
382
381
|
metadata (dict | None): The metadata to associate with the content. Defaults to None.
|
|
383
382
|
|
|
@@ -449,7 +448,7 @@ class KnowledgeBaseService:
|
|
|
449
448
|
skip_excel_ingestion: bool = False,
|
|
450
449
|
ingestion_config: unique_sdk.Content.IngestionConfig | None = None,
|
|
451
450
|
metadata: dict[str, Any] | None = None,
|
|
452
|
-
):
|
|
451
|
+
) -> Content:
|
|
453
452
|
"""
|
|
454
453
|
Uploads content to the knowledge base.
|
|
455
454
|
|
|
@@ -487,14 +486,14 @@ class KnowledgeBaseService:
|
|
|
487
486
|
content_id: str,
|
|
488
487
|
output_dir_path: Path | None = None,
|
|
489
488
|
output_filename: str | None = None,
|
|
490
|
-
):
|
|
489
|
+
) -> Path:
|
|
491
490
|
"""
|
|
492
491
|
Downloads content from a chat and saves it to a file.
|
|
493
492
|
|
|
494
493
|
Args:
|
|
495
494
|
content_id (str): The ID of the content to download.
|
|
496
|
-
|
|
497
|
-
|
|
495
|
+
output_filename (str | None): The name of the file to save the content as. If not provided, the original filename will be used. Defaults to None.
|
|
496
|
+
output_dir_path (str | Path | None): The path to the temporary directory where the content will be saved. Defaults to "/tmp".
|
|
498
497
|
|
|
499
498
|
Returns:
|
|
500
499
|
Path: The path to the downloaded file.
|
|
@@ -522,7 +521,6 @@ class KnowledgeBaseService:
|
|
|
522
521
|
|
|
523
522
|
Args:
|
|
524
523
|
content_id (str): The id of the uploaded content.
|
|
525
|
-
chat_id (Optional[str]): The chat_id, defaults to None.
|
|
526
524
|
|
|
527
525
|
Returns:
|
|
528
526
|
bytes: The downloaded content.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: unique_toolkit
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.32.1
|
|
4
4
|
Summary:
|
|
5
5
|
License: Proprietary
|
|
6
6
|
Author: Cedric Klinkert
|
|
@@ -121,6 +121,12 @@ All notable changes to this project will be documented in this file.
|
|
|
121
121
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
122
122
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
123
123
|
|
|
124
|
+
## [1.32.1] - 2025-12-01
|
|
125
|
+
- Added documentation for the toolkit,some missing type hints and doc string fixes.
|
|
126
|
+
|
|
127
|
+
## [1.32.0] - 2025-11-28
|
|
128
|
+
- Add option to filter duplicate sub agent answers.
|
|
129
|
+
|
|
124
130
|
## [1.31.2] - 2025-11-27
|
|
125
131
|
- Added the function `filter_tool_calls_by_max_tool_calls_allowed` in `tool_manager` to limit the number of parallel tool calls permitted per loop iteration.
|
|
126
132
|
|
|
@@ -82,12 +82,13 @@ unique_toolkit/agentic/tools/a2a/evaluation/evaluator.py,sha256=AJvXu0UJKHe72nRm
|
|
|
82
82
|
unique_toolkit/agentic/tools/a2a/evaluation/summarization_user_message.j2,sha256=acP1YqD_sCy6DT0V2EIfhQTmaUKeqpeWNJ7RGgceo8I,271
|
|
83
83
|
unique_toolkit/agentic/tools/a2a/manager.py,sha256=pk06UUXKQdIUY-PyykYiItubBjmIydOaqWvBBDwhMN4,1939
|
|
84
84
|
unique_toolkit/agentic/tools/a2a/postprocessing/__init__.py,sha256=aVtUBPN7kDrqA6Bze34AbqQpcBBqpvfyJG-xF65w7R0,659
|
|
85
|
-
unique_toolkit/agentic/tools/a2a/postprocessing/_display_utils.py,sha256=
|
|
86
|
-
unique_toolkit/agentic/tools/a2a/postprocessing/_ref_utils.py,sha256=
|
|
85
|
+
unique_toolkit/agentic/tools/a2a/postprocessing/_display_utils.py,sha256=eL_5bkud6aPCKGER705ZQkNttrUZEs-zco83SfbffDQ,7107
|
|
86
|
+
unique_toolkit/agentic/tools/a2a/postprocessing/_ref_utils.py,sha256=tfH3Bhx67uQtH3fYoUr2Zp45HVw7hV_N-5F_qRwiOas,2727
|
|
87
87
|
unique_toolkit/agentic/tools/a2a/postprocessing/config.py,sha256=nYH1otBrVGHq1Y2i1KII3pb1IVE0efwmOwhOLLsv1qU,2841
|
|
88
|
-
unique_toolkit/agentic/tools/a2a/postprocessing/display.py,sha256=
|
|
88
|
+
unique_toolkit/agentic/tools/a2a/postprocessing/display.py,sha256=e8R92XJHwyVI8UxVqCG_cTzensHSfGOIfDe1nfGcDoE,9058
|
|
89
89
|
unique_toolkit/agentic/tools/a2a/postprocessing/references.py,sha256=DGiv8WXMjIwumI7tlpWRgV8wSxnE282ryxEf03fgck8,3465
|
|
90
|
-
unique_toolkit/agentic/tools/a2a/postprocessing/test/
|
|
90
|
+
unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display.py,sha256=pP7fjvCm4aamtizs4viZSwsrw4Vb4kMxwDPEEx8GI98,14676
|
|
91
|
+
unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display_utils.py,sha256=5d67L4msesDENsNhgeoDTH6pjw_Z4FUUC1lRM9j4PlI,63608
|
|
91
92
|
unique_toolkit/agentic/tools/a2a/postprocessing/test/test_ref_utils.py,sha256=u-eNrHjsRFcu4TmzkD5XfFrxvaIToB42YGyXZ-RpsR0,17830
|
|
92
93
|
unique_toolkit/agentic/tools/a2a/prompts.py,sha256=0ILHL_RAcT04gFm2d470j4Gho7PoJXdCJy-bkZgf_wk,2401
|
|
93
94
|
unique_toolkit/agentic/tools/a2a/response_watcher/__init__.py,sha256=fS8Lq49DZo5spMcP8QGTMWwSg80rYSr2pTfYDbssGYs,184
|
|
@@ -137,7 +138,7 @@ unique_toolkit/app/webhook.py,sha256=k7DP1UTR3p7D4qzuKPKVmGMAkDVHfALrnMIzTZqj_OI
|
|
|
137
138
|
unique_toolkit/chat/__init__.py,sha256=uP7P6YPeOjEOvpX3bhcU6ND_m0QLr4wMklcrnAKK0q4,804
|
|
138
139
|
unique_toolkit/chat/constants.py,sha256=05kq6zjqUVB2d6_P7s-90nbljpB3ryxwCI-CAz0r2O4,83
|
|
139
140
|
unique_toolkit/chat/deprecated/service.py,sha256=CYwzXi7OB0RjHd73CO2jq8SlpdBmDYLatzPFkb5sA0k,6529
|
|
140
|
-
unique_toolkit/chat/functions.py,sha256=
|
|
141
|
+
unique_toolkit/chat/functions.py,sha256=VdehD26NnsEhBsV5tD0tgUFD4LuEqIYgz8wWSsxYFgA,46078
|
|
141
142
|
unique_toolkit/chat/rendering.py,sha256=c8YiV9oADRrJQ5A_QBJ4_UFc0NZ-2vVaa7tupoMusso,880
|
|
142
143
|
unique_toolkit/chat/responses_api.py,sha256=MCI1MR_4wlo9xY1ifH2daNz4JvjX18uPdryQlemaeLw,14488
|
|
143
144
|
unique_toolkit/chat/schemas.py,sha256=Zh903Xt9PhugvwzoC_k5KXQWAqllWHvj9DmqDVom2yk,6890
|
|
@@ -146,22 +147,23 @@ unique_toolkit/chat/state.py,sha256=Cjgwv_2vhDFbV69xxsn7SefhaoIAEqLx3ferdVFCnOg,
|
|
|
146
147
|
unique_toolkit/chat/utils.py,sha256=ihm-wQykBWhB4liR3LnwPVPt_qGW6ETq21Mw4HY0THE,854
|
|
147
148
|
unique_toolkit/content/__init__.py,sha256=EdJg_A_7loEtCQf4cah3QARQreJx6pdz89Rm96YbMVg,940
|
|
148
149
|
unique_toolkit/content/constants.py,sha256=1iy4Y67xobl5VTnJB6SxSyuoBWbdLl9244xfVMUZi5o,60
|
|
149
|
-
unique_toolkit/content/functions.py,sha256=
|
|
150
|
+
unique_toolkit/content/functions.py,sha256=6QIgPTShF7VF9N55Np5vIlNibbDgPDfJr4-IBaRu6n8,28830
|
|
150
151
|
unique_toolkit/content/schemas.py,sha256=uuS1UsuWK6eC7cP4dTC1q3DJ39xl6zenN2zL4ghFmzk,6424
|
|
151
|
-
unique_toolkit/content/service.py,sha256=
|
|
152
|
+
unique_toolkit/content/service.py,sha256=hwycIbxtLn1p0IgNQMVIxN2NUhy_4AVsTfatytGi-gY,24919
|
|
152
153
|
unique_toolkit/content/smart_rules.py,sha256=z2gHToPrdyj3HqO8Uu-JE5G2ClvJPuhR2XERmmkgoug,9668
|
|
153
154
|
unique_toolkit/content/utils.py,sha256=ITCJHDIXPyFkxR5M-6k-PhHOLbnkVq_RGrGA7i5s1WM,8001
|
|
154
155
|
unique_toolkit/embedding/__init__.py,sha256=uUyzjonPvuDCYsvXCIt7ErQXopLggpzX-MEQd3_e2kE,250
|
|
155
156
|
unique_toolkit/embedding/constants.py,sha256=Lj8-Lcy1FvuC31PM9Exq7vaFuxQV4pEI1huUMFX-J2M,52
|
|
156
157
|
unique_toolkit/embedding/functions.py,sha256=3qp-BfuMAbnp8YB04rh3xH8vsJuCBPizoy-JeaBFtoQ,1944
|
|
157
158
|
unique_toolkit/embedding/schemas.py,sha256=1GvKCaSk4jixzVQ2PKq8yDqwGEVY_hWclYtoAr6CC2g,96
|
|
158
|
-
unique_toolkit/embedding/service.py,sha256=
|
|
159
|
+
unique_toolkit/embedding/service.py,sha256=Ujx4oy7yUsQot8ATyl-3x88LulgruPThEteSArwzuvA,5291
|
|
159
160
|
unique_toolkit/embedding/utils.py,sha256=v86lo__bCJbxZBQ3OcLu5SuwT6NbFfWlcq8iyk6BuzQ,279
|
|
160
161
|
unique_toolkit/framework_utilities/__init__.py,sha256=fvAn9y4MRL1JgoO14ufQtLVRPRHn4jP07XRqt-TItCA,68
|
|
162
|
+
unique_toolkit/framework_utilities/langchain/__init__.py,sha256=CjihxDQqCeSiRpTy1fSRRMYXXKrvhNuChR8nbUlIm84,362
|
|
161
163
|
unique_toolkit/framework_utilities/langchain/client.py,sha256=NxOLnzVMJds2JFMm5ZqTVTz1WMntrmWsLPU4-kwlzqE,2065
|
|
162
164
|
unique_toolkit/framework_utilities/langchain/history.py,sha256=R9RuCeSFNaUO3OZ0G_LmIC4gmOCIANcl91MfyWLnZ1c,650
|
|
163
165
|
unique_toolkit/framework_utilities/openai/__init__.py,sha256=CrHYoC7lv2pBscitLerAFweqy5jh1R671LA_jZQ4lWo,232
|
|
164
|
-
unique_toolkit/framework_utilities/openai/client.py,sha256=
|
|
166
|
+
unique_toolkit/framework_utilities/openai/client.py,sha256=Ft-oLiS6BTaMiuCdObdaMLkiEPE3RYjtc9VPnqQk3W8,2449
|
|
165
167
|
unique_toolkit/framework_utilities/openai/message_builder.py,sha256=RT1pZjxH42TFZlAxQ5zlqdKPvHKVTjc5t3JDUy58I7Q,6887
|
|
166
168
|
unique_toolkit/framework_utilities/utils.py,sha256=JK7g2yMfEx3eMprug26769xqNpS5WJcizf8n2zWMBng,789
|
|
167
169
|
unique_toolkit/language_model/__init__.py,sha256=lRQyLlbwHbNFf4-0foBU13UGb09lwEeodbVsfsSgaCk,1971
|
|
@@ -179,7 +181,7 @@ unique_toolkit/language_model/utils.py,sha256=bPQ4l6_YO71w-zaIPanUUmtbXC1_hCvLK0
|
|
|
179
181
|
unique_toolkit/protocols/support.py,sha256=ZEnbQL5w2-T_1AeM8OHycZJ3qbdfVI1nXe0nL9esQEw,5544
|
|
180
182
|
unique_toolkit/services/__init__.py,sha256=90-IT5FjMcnlqxjp5kme9Fqgp_on46rggctIqHMdqsw,195
|
|
181
183
|
unique_toolkit/services/chat_service.py,sha256=vA97DO4iiXMeTNuwS-bYeskX0DnqO_8vL4yzJAfsGtw,60111
|
|
182
|
-
unique_toolkit/services/knowledge_base.py,sha256=
|
|
184
|
+
unique_toolkit/services/knowledge_base.py,sha256=uc89GL_NZXeFkJKkdHSSh2y1Wx0tmgasWk6uyGi4G_M,36210
|
|
183
185
|
unique_toolkit/short_term_memory/__init__.py,sha256=2mI3AUrffgH7Yt-xS57EGqnHf7jnn6xquoKEhJqk3Wg,185
|
|
184
186
|
unique_toolkit/short_term_memory/constants.py,sha256=698CL6-wjup2MvU19RxSmQk3gX7aqW_OOpZB7sbz_Xg,34
|
|
185
187
|
unique_toolkit/short_term_memory/functions.py,sha256=3WiK-xatY5nh4Dr5zlDUye1k3E6kr41RiscwtTplw5k,4484
|
|
@@ -188,7 +190,7 @@ unique_toolkit/short_term_memory/service.py,sha256=5PeVBu1ZCAfyDb2HLVvlmqSbyzBBu
|
|
|
188
190
|
unique_toolkit/smart_rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
189
191
|
unique_toolkit/smart_rules/compile.py,sha256=Ozhh70qCn2yOzRWr9d8WmJeTo7AQurwd3tStgBMPFLA,1246
|
|
190
192
|
unique_toolkit/test_utilities/events.py,sha256=_mwV2bs5iLjxS1ynDCjaIq-gjjKhXYCK-iy3dRfvO3g,6410
|
|
191
|
-
unique_toolkit-1.
|
|
192
|
-
unique_toolkit-1.
|
|
193
|
-
unique_toolkit-1.
|
|
194
|
-
unique_toolkit-1.
|
|
193
|
+
unique_toolkit-1.32.1.dist-info/LICENSE,sha256=GlN8wHNdh53xwOPg44URnwag6TEolCjoq3YD_KrWgss,193
|
|
194
|
+
unique_toolkit-1.32.1.dist-info/METADATA,sha256=ar50wEkaXnpqXhlgJHki4YZIJG6SQFjzXMcfH4w-_W0,45088
|
|
195
|
+
unique_toolkit-1.32.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
196
|
+
unique_toolkit-1.32.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|