unique_toolkit 0.7.9__py3-none-any.whl → 1.33.3__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.
Files changed (190) hide show
  1. unique_toolkit/__init__.py +36 -3
  2. unique_toolkit/_common/api_calling/human_verification_manager.py +357 -0
  3. unique_toolkit/_common/base_model_type_attribute.py +303 -0
  4. unique_toolkit/_common/chunk_relevancy_sorter/config.py +49 -0
  5. unique_toolkit/_common/chunk_relevancy_sorter/exception.py +5 -0
  6. unique_toolkit/_common/chunk_relevancy_sorter/schemas.py +46 -0
  7. unique_toolkit/_common/chunk_relevancy_sorter/service.py +374 -0
  8. unique_toolkit/_common/chunk_relevancy_sorter/tests/test_service.py +275 -0
  9. unique_toolkit/_common/default_language_model.py +12 -0
  10. unique_toolkit/_common/docx_generator/__init__.py +7 -0
  11. unique_toolkit/_common/docx_generator/config.py +12 -0
  12. unique_toolkit/_common/docx_generator/schemas.py +80 -0
  13. unique_toolkit/_common/docx_generator/service.py +225 -0
  14. unique_toolkit/_common/docx_generator/template/Doc Template.docx +0 -0
  15. unique_toolkit/_common/endpoint_builder.py +368 -0
  16. unique_toolkit/_common/endpoint_requestor.py +480 -0
  17. unique_toolkit/_common/exception.py +24 -0
  18. unique_toolkit/_common/experimental/endpoint_builder.py +368 -0
  19. unique_toolkit/_common/experimental/endpoint_requestor.py +488 -0
  20. unique_toolkit/_common/feature_flags/schema.py +9 -0
  21. unique_toolkit/_common/pydantic/rjsf_tags.py +936 -0
  22. unique_toolkit/_common/pydantic_helpers.py +174 -0
  23. unique_toolkit/_common/referencing.py +53 -0
  24. unique_toolkit/_common/string_utilities.py +140 -0
  25. unique_toolkit/_common/tests/test_referencing.py +521 -0
  26. unique_toolkit/_common/tests/test_string_utilities.py +506 -0
  27. unique_toolkit/_common/token/image_token_counting.py +67 -0
  28. unique_toolkit/_common/token/token_counting.py +204 -0
  29. unique_toolkit/_common/utils/__init__.py +1 -0
  30. unique_toolkit/_common/utils/files.py +43 -0
  31. unique_toolkit/_common/utils/image/encode.py +25 -0
  32. unique_toolkit/_common/utils/jinja/helpers.py +10 -0
  33. unique_toolkit/_common/utils/jinja/render.py +18 -0
  34. unique_toolkit/_common/utils/jinja/schema.py +65 -0
  35. unique_toolkit/_common/utils/jinja/utils.py +80 -0
  36. unique_toolkit/_common/utils/structured_output/__init__.py +1 -0
  37. unique_toolkit/_common/utils/structured_output/schema.py +5 -0
  38. unique_toolkit/_common/utils/write_configuration.py +51 -0
  39. unique_toolkit/_common/validators.py +101 -4
  40. unique_toolkit/agentic/__init__.py +1 -0
  41. unique_toolkit/agentic/debug_info_manager/debug_info_manager.py +28 -0
  42. unique_toolkit/agentic/debug_info_manager/test/test_debug_info_manager.py +278 -0
  43. unique_toolkit/agentic/evaluation/config.py +36 -0
  44. unique_toolkit/{evaluators → agentic/evaluation}/context_relevancy/prompts.py +25 -0
  45. unique_toolkit/agentic/evaluation/context_relevancy/schema.py +80 -0
  46. unique_toolkit/agentic/evaluation/context_relevancy/service.py +273 -0
  47. unique_toolkit/agentic/evaluation/evaluation_manager.py +218 -0
  48. unique_toolkit/agentic/evaluation/hallucination/constants.py +61 -0
  49. unique_toolkit/agentic/evaluation/hallucination/hallucination_evaluation.py +112 -0
  50. unique_toolkit/{evaluators → agentic/evaluation}/hallucination/prompts.py +1 -1
  51. unique_toolkit/{evaluators → agentic/evaluation}/hallucination/service.py +20 -16
  52. unique_toolkit/{evaluators → agentic/evaluation}/hallucination/utils.py +32 -21
  53. unique_toolkit/{evaluators → agentic/evaluation}/output_parser.py +20 -2
  54. unique_toolkit/{evaluators → agentic/evaluation}/schemas.py +27 -7
  55. unique_toolkit/agentic/evaluation/tests/test_context_relevancy_service.py +253 -0
  56. unique_toolkit/agentic/evaluation/tests/test_output_parser.py +87 -0
  57. unique_toolkit/agentic/history_manager/history_construction_with_contents.py +298 -0
  58. unique_toolkit/agentic/history_manager/history_manager.py +241 -0
  59. unique_toolkit/agentic/history_manager/loop_token_reducer.py +484 -0
  60. unique_toolkit/agentic/history_manager/utils.py +96 -0
  61. unique_toolkit/agentic/message_log_manager/__init__.py +5 -0
  62. unique_toolkit/agentic/message_log_manager/service.py +93 -0
  63. unique_toolkit/agentic/postprocessor/postprocessor_manager.py +212 -0
  64. unique_toolkit/agentic/reference_manager/reference_manager.py +103 -0
  65. unique_toolkit/agentic/responses_api/__init__.py +19 -0
  66. unique_toolkit/agentic/responses_api/postprocessors/code_display.py +71 -0
  67. unique_toolkit/agentic/responses_api/postprocessors/generated_files.py +297 -0
  68. unique_toolkit/agentic/responses_api/stream_handler.py +15 -0
  69. unique_toolkit/agentic/short_term_memory_manager/persistent_short_term_memory_manager.py +141 -0
  70. unique_toolkit/agentic/thinking_manager/thinking_manager.py +103 -0
  71. unique_toolkit/agentic/tools/__init__.py +1 -0
  72. unique_toolkit/agentic/tools/a2a/__init__.py +36 -0
  73. unique_toolkit/agentic/tools/a2a/config.py +17 -0
  74. unique_toolkit/agentic/tools/a2a/evaluation/__init__.py +15 -0
  75. unique_toolkit/agentic/tools/a2a/evaluation/_utils.py +66 -0
  76. unique_toolkit/agentic/tools/a2a/evaluation/config.py +55 -0
  77. unique_toolkit/agentic/tools/a2a/evaluation/evaluator.py +260 -0
  78. unique_toolkit/agentic/tools/a2a/evaluation/summarization_user_message.j2 +9 -0
  79. unique_toolkit/agentic/tools/a2a/manager.py +55 -0
  80. unique_toolkit/agentic/tools/a2a/postprocessing/__init__.py +21 -0
  81. unique_toolkit/agentic/tools/a2a/postprocessing/_display_utils.py +240 -0
  82. unique_toolkit/agentic/tools/a2a/postprocessing/_ref_utils.py +84 -0
  83. unique_toolkit/agentic/tools/a2a/postprocessing/config.py +78 -0
  84. unique_toolkit/agentic/tools/a2a/postprocessing/display.py +264 -0
  85. unique_toolkit/agentic/tools/a2a/postprocessing/references.py +101 -0
  86. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display.py +421 -0
  87. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display_utils.py +2103 -0
  88. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_ref_utils.py +603 -0
  89. unique_toolkit/agentic/tools/a2a/prompts.py +46 -0
  90. unique_toolkit/agentic/tools/a2a/response_watcher/__init__.py +6 -0
  91. unique_toolkit/agentic/tools/a2a/response_watcher/service.py +91 -0
  92. unique_toolkit/agentic/tools/a2a/tool/__init__.py +4 -0
  93. unique_toolkit/agentic/tools/a2a/tool/_memory.py +26 -0
  94. unique_toolkit/agentic/tools/a2a/tool/_schema.py +9 -0
  95. unique_toolkit/agentic/tools/a2a/tool/config.py +158 -0
  96. unique_toolkit/agentic/tools/a2a/tool/service.py +393 -0
  97. unique_toolkit/agentic/tools/agent_chunks_hanlder.py +65 -0
  98. unique_toolkit/agentic/tools/config.py +128 -0
  99. unique_toolkit/agentic/tools/factory.py +44 -0
  100. unique_toolkit/agentic/tools/mcp/__init__.py +4 -0
  101. unique_toolkit/agentic/tools/mcp/manager.py +71 -0
  102. unique_toolkit/agentic/tools/mcp/models.py +28 -0
  103. unique_toolkit/agentic/tools/mcp/tool_wrapper.py +234 -0
  104. unique_toolkit/agentic/tools/openai_builtin/__init__.py +11 -0
  105. unique_toolkit/agentic/tools/openai_builtin/base.py +46 -0
  106. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/__init__.py +8 -0
  107. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/config.py +88 -0
  108. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/service.py +250 -0
  109. unique_toolkit/agentic/tools/openai_builtin/manager.py +79 -0
  110. unique_toolkit/agentic/tools/schemas.py +145 -0
  111. unique_toolkit/agentic/tools/test/test_mcp_manager.py +536 -0
  112. unique_toolkit/agentic/tools/test/test_tool_progress_reporter.py +445 -0
  113. unique_toolkit/agentic/tools/tool.py +187 -0
  114. unique_toolkit/agentic/tools/tool_manager.py +492 -0
  115. unique_toolkit/agentic/tools/tool_progress_reporter.py +285 -0
  116. unique_toolkit/agentic/tools/utils/__init__.py +19 -0
  117. unique_toolkit/agentic/tools/utils/execution/__init__.py +1 -0
  118. unique_toolkit/agentic/tools/utils/execution/execution.py +286 -0
  119. unique_toolkit/agentic/tools/utils/source_handling/__init__.py +0 -0
  120. unique_toolkit/agentic/tools/utils/source_handling/schema.py +21 -0
  121. unique_toolkit/agentic/tools/utils/source_handling/source_formatting.py +207 -0
  122. unique_toolkit/agentic/tools/utils/source_handling/tests/test_source_formatting.py +216 -0
  123. unique_toolkit/app/__init__.py +9 -0
  124. unique_toolkit/app/dev_util.py +180 -0
  125. unique_toolkit/app/fast_api_factory.py +131 -0
  126. unique_toolkit/app/init_sdk.py +32 -1
  127. unique_toolkit/app/schemas.py +206 -31
  128. unique_toolkit/app/unique_settings.py +367 -0
  129. unique_toolkit/app/webhook.py +77 -0
  130. unique_toolkit/chat/__init__.py +8 -1
  131. unique_toolkit/chat/deprecated/service.py +232 -0
  132. unique_toolkit/chat/functions.py +648 -78
  133. unique_toolkit/chat/rendering.py +34 -0
  134. unique_toolkit/chat/responses_api.py +461 -0
  135. unique_toolkit/chat/schemas.py +134 -2
  136. unique_toolkit/chat/service.py +115 -767
  137. unique_toolkit/content/functions.py +353 -8
  138. unique_toolkit/content/schemas.py +128 -15
  139. unique_toolkit/content/service.py +321 -45
  140. unique_toolkit/content/smart_rules.py +301 -0
  141. unique_toolkit/content/utils.py +10 -3
  142. unique_toolkit/data_extraction/README.md +96 -0
  143. unique_toolkit/data_extraction/__init__.py +11 -0
  144. unique_toolkit/data_extraction/augmented/__init__.py +5 -0
  145. unique_toolkit/data_extraction/augmented/service.py +93 -0
  146. unique_toolkit/data_extraction/base.py +25 -0
  147. unique_toolkit/data_extraction/basic/__init__.py +11 -0
  148. unique_toolkit/data_extraction/basic/config.py +18 -0
  149. unique_toolkit/data_extraction/basic/prompt.py +13 -0
  150. unique_toolkit/data_extraction/basic/service.py +55 -0
  151. unique_toolkit/embedding/service.py +103 -12
  152. unique_toolkit/framework_utilities/__init__.py +1 -0
  153. unique_toolkit/framework_utilities/langchain/__init__.py +10 -0
  154. unique_toolkit/framework_utilities/langchain/client.py +71 -0
  155. unique_toolkit/framework_utilities/langchain/history.py +19 -0
  156. unique_toolkit/framework_utilities/openai/__init__.py +6 -0
  157. unique_toolkit/framework_utilities/openai/client.py +84 -0
  158. unique_toolkit/framework_utilities/openai/message_builder.py +229 -0
  159. unique_toolkit/framework_utilities/utils.py +23 -0
  160. unique_toolkit/language_model/__init__.py +3 -0
  161. unique_toolkit/language_model/_responses_api_utils.py +93 -0
  162. unique_toolkit/language_model/builder.py +27 -11
  163. unique_toolkit/language_model/default_language_model.py +3 -0
  164. unique_toolkit/language_model/functions.py +345 -43
  165. unique_toolkit/language_model/infos.py +1288 -46
  166. unique_toolkit/language_model/reference.py +242 -0
  167. unique_toolkit/language_model/schemas.py +481 -49
  168. unique_toolkit/language_model/service.py +229 -28
  169. unique_toolkit/protocols/support.py +145 -0
  170. unique_toolkit/services/__init__.py +7 -0
  171. unique_toolkit/services/chat_service.py +1631 -0
  172. unique_toolkit/services/knowledge_base.py +1094 -0
  173. unique_toolkit/short_term_memory/service.py +178 -41
  174. unique_toolkit/smart_rules/__init__.py +0 -0
  175. unique_toolkit/smart_rules/compile.py +56 -0
  176. unique_toolkit/test_utilities/events.py +197 -0
  177. unique_toolkit-1.33.3.dist-info/METADATA +1145 -0
  178. unique_toolkit-1.33.3.dist-info/RECORD +205 -0
  179. unique_toolkit/evaluators/__init__.py +0 -1
  180. unique_toolkit/evaluators/config.py +0 -35
  181. unique_toolkit/evaluators/constants.py +0 -1
  182. unique_toolkit/evaluators/context_relevancy/constants.py +0 -32
  183. unique_toolkit/evaluators/context_relevancy/service.py +0 -53
  184. unique_toolkit/evaluators/context_relevancy/utils.py +0 -142
  185. unique_toolkit/evaluators/hallucination/constants.py +0 -41
  186. unique_toolkit-0.7.9.dist-info/METADATA +0 -413
  187. unique_toolkit-0.7.9.dist-info/RECORD +0 -64
  188. /unique_toolkit/{evaluators → agentic/evaluation}/exception.py +0 -0
  189. {unique_toolkit-0.7.9.dist-info → unique_toolkit-1.33.3.dist-info}/LICENSE +0 -0
  190. {unique_toolkit-0.7.9.dist-info → unique_toolkit-1.33.3.dist-info}/WHEEL +0 -0
@@ -0,0 +1,91 @@
1
+ import datetime
2
+ import json
3
+ from typing import NamedTuple
4
+
5
+ import unique_sdk
6
+
7
+
8
+ def _clone_message(
9
+ message: unique_sdk.Space.Message,
10
+ ) -> unique_sdk.Space.Message:
11
+ # copy.deepcopy does not work for instances of UniqueObject
12
+ return json.loads(json.dumps(message))
13
+
14
+
15
+ class SubAgentResponse(NamedTuple):
16
+ assistant_id: str
17
+ name: str
18
+ sequence_number: int
19
+ message: unique_sdk.Space.Message
20
+ timestamp: datetime.datetime
21
+
22
+ def clone(self) -> "SubAgentResponse":
23
+ return SubAgentResponse(
24
+ assistant_id=self.assistant_id,
25
+ name=self.name,
26
+ sequence_number=self.sequence_number,
27
+ message=_clone_message(self.message),
28
+ timestamp=self.timestamp,
29
+ )
30
+
31
+
32
+ class SubAgentResponseWatcher:
33
+ """
34
+ Save and retrieve sub agent responses immutably.
35
+ """
36
+
37
+ def __init__(self) -> None:
38
+ self._response_registry: dict[str, list[SubAgentResponse]] = {}
39
+
40
+ def notify_response(
41
+ self,
42
+ assistant_id: str,
43
+ name: str,
44
+ sequence_number: int,
45
+ response: unique_sdk.Space.Message,
46
+ timestamp: datetime.datetime,
47
+ ) -> None:
48
+ if assistant_id not in self._response_registry:
49
+ self._response_registry[assistant_id] = []
50
+
51
+ response = _clone_message(response)
52
+
53
+ self._response_registry[assistant_id].append(
54
+ SubAgentResponse(
55
+ assistant_id=assistant_id,
56
+ name=name,
57
+ sequence_number=sequence_number,
58
+ message=response,
59
+ timestamp=timestamp,
60
+ )
61
+ )
62
+
63
+ def get_responses(self, assistant_id: str) -> list[SubAgentResponse]:
64
+ return _sort_responses( # Always return a consistent order
65
+ [
66
+ response.clone()
67
+ for response in self._response_registry.get(assistant_id, [])
68
+ ],
69
+ )
70
+
71
+ def get_all_responses(self) -> list[SubAgentResponse]:
72
+ return _sort_responses(
73
+ [
74
+ response.clone()
75
+ for sub_agent_responses in self._response_registry.values()
76
+ for response in sub_agent_responses
77
+ ],
78
+ )
79
+
80
+
81
+ def _sort_responses(
82
+ responses: list[SubAgentResponse],
83
+ ) -> list[SubAgentResponse]:
84
+ return sorted(
85
+ responses,
86
+ key=lambda response: (
87
+ response.timestamp,
88
+ response.assistant_id,
89
+ response.sequence_number,
90
+ ),
91
+ )
@@ -0,0 +1,4 @@
1
+ from unique_toolkit.agentic.tools.a2a.tool.config import SubAgentToolConfig
2
+ from unique_toolkit.agentic.tools.a2a.tool.service import SubAgentTool
3
+
4
+ __all__ = ["SubAgentTool", "SubAgentToolConfig"]
@@ -0,0 +1,26 @@
1
+ from unique_toolkit import ShortTermMemoryService
2
+ from unique_toolkit.agentic.short_term_memory_manager.persistent_short_term_memory_manager import (
3
+ PersistentShortMemoryManager,
4
+ )
5
+ from unique_toolkit.agentic.tools.a2a.tool._schema import SubAgentShortTermMemorySchema
6
+
7
+
8
+ def _get_short_term_memory_name(assistant_id: str) -> str:
9
+ return f"sub_agent_chat_id_{assistant_id}"
10
+
11
+
12
+ def get_sub_agent_short_term_memory_manager(
13
+ company_id: str, user_id: str, chat_id: str, assistant_id: str
14
+ ) -> PersistentShortMemoryManager[SubAgentShortTermMemorySchema]:
15
+ short_term_memory_service = ShortTermMemoryService(
16
+ company_id=company_id,
17
+ user_id=user_id,
18
+ chat_id=chat_id,
19
+ message_id=None,
20
+ )
21
+ short_term_memory_manager = PersistentShortMemoryManager(
22
+ short_term_memory_service=short_term_memory_service,
23
+ short_term_memory_schema=SubAgentShortTermMemorySchema,
24
+ short_term_memory_name=_get_short_term_memory_name(assistant_id),
25
+ )
26
+ return short_term_memory_manager
@@ -0,0 +1,9 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class SubAgentToolInput(BaseModel):
5
+ user_message: str
6
+
7
+
8
+ class SubAgentShortTermMemorySchema(BaseModel):
9
+ chat_id: str
@@ -0,0 +1,158 @@
1
+ import re
2
+ from enum import StrEnum
3
+ from typing import Annotated, Generic, Literal, TypeVar
4
+
5
+ from pydantic import Field
6
+ from pydantic.main import BaseModel
7
+
8
+ from unique_toolkit._common.pydantic_helpers import get_configuration_dict
9
+ from unique_toolkit.agentic.tools.schemas import BaseToolConfig
10
+
11
+
12
+ class SubAgentSystemReminderType(StrEnum):
13
+ FIXED = "fixed"
14
+ REGEXP = "regexp"
15
+ REFERENCE = "reference"
16
+
17
+
18
+ T = TypeVar("T", bound=SubAgentSystemReminderType)
19
+
20
+
21
+ class SystemReminderConfig(BaseModel, Generic[T]):
22
+ model_config = get_configuration_dict()
23
+
24
+ type: T
25
+
26
+
27
+ _SYSTEM_REMINDER_FIELD_DESCRIPTION = """
28
+ The reminder to add to the tool response. The reminder can be a Jinja template and can contain the following placeholders:
29
+ - {{ display_name }}: The display name of the sub agent.
30
+ - {{ tool_name }}: The tool name.
31
+ """.strip()
32
+
33
+
34
+ class ReferenceSystemReminderConfig(SystemReminderConfig):
35
+ type: Literal[SubAgentSystemReminderType.REFERENCE] = (
36
+ SubAgentSystemReminderType.REFERENCE
37
+ )
38
+ reminder: str = Field(
39
+ default="Rememeber to properly reference EACH fact from sub agent {{ display_name }}'s response with the correct format INLINE.",
40
+ description=_SYSTEM_REMINDER_FIELD_DESCRIPTION,
41
+ )
42
+
43
+
44
+ class FixedSystemReminderConfig(SystemReminderConfig):
45
+ type: Literal[SubAgentSystemReminderType.FIXED] = SubAgentSystemReminderType.FIXED
46
+ reminder: str = Field(
47
+ description=_SYSTEM_REMINDER_FIELD_DESCRIPTION,
48
+ )
49
+
50
+
51
+ _REGEXP_DETECTED_REMINDER_FIELD_DESCRIPTION = """
52
+ The reminder to add to the tool response. The reminder can be a Jinja template and can contain the following placeholders:
53
+ - {{ display_name }}: The display name of the sub agent.
54
+ - {{ tool_name }}: The tool name.
55
+ - {{ text_matches }}: Will be replaced with the portions of the text that triggered the reminder.
56
+ """.strip()
57
+
58
+
59
+ class RegExpDetectedSystemReminderConfig(SystemReminderConfig):
60
+ """A system reminder that is only added if the sub agent response matches a regular expression."""
61
+
62
+ type: Literal[SubAgentSystemReminderType.REGEXP] = SubAgentSystemReminderType.REGEXP
63
+
64
+ regexp: re.Pattern[str] = Field(
65
+ description="The regular expression to use to detect whether the system reminder should be added.",
66
+ )
67
+ reminder: str = Field(
68
+ description=_REGEXP_DETECTED_REMINDER_FIELD_DESCRIPTION,
69
+ )
70
+
71
+
72
+ DEFAULT_PARAM_DESCRIPTION_SUB_AGENT_USER_MESSAGE = """
73
+ This is the message that will be sent to the sub-agent.
74
+ """.strip()
75
+
76
+
77
+ class SubAgentToolConfig(BaseToolConfig):
78
+ model_config = get_configuration_dict()
79
+
80
+ assistant_id: str = Field(
81
+ default="",
82
+ description="The unique identifier of the assistant to use for the sub-agent.",
83
+ )
84
+ chat_id: str | None = Field(
85
+ default=None,
86
+ description="The chat ID to use for the sub-agent conversation. If None, a new chat will be created.",
87
+ )
88
+ reuse_chat: bool = Field(
89
+ default=True,
90
+ description="Whether to reuse the existing chat or create a new one for each sub-agent call.",
91
+ )
92
+ use_sub_agent_references: bool = Field(
93
+ default=True,
94
+ description="Whether this sub agent's references should be used in the main agent's response.",
95
+ )
96
+ forced_tools: list[str] | None = Field(
97
+ default=None,
98
+ description="The list of tool names that will be forced to be called for this sub-agent.",
99
+ )
100
+
101
+ tool_description_for_system_prompt: str = Field(
102
+ default="",
103
+ description="Description of the tool that will be included in the system prompt.",
104
+ )
105
+ tool_description: str = Field(
106
+ default="",
107
+ description="Description of the tool that will be included in the tools sent to the model.",
108
+ )
109
+ param_description_sub_agent_user_message: str = Field(
110
+ default=DEFAULT_PARAM_DESCRIPTION_SUB_AGENT_USER_MESSAGE,
111
+ description="Description of the user message parameter that will be sent to the model.",
112
+ )
113
+ tool_format_information_for_system_prompt: str = Field(
114
+ default="",
115
+ description="Format information that will be included in the system prompt to guide response formatting.",
116
+ )
117
+ tool_description_for_user_prompt: str = Field(
118
+ default="",
119
+ description="Description of the tool that will be included in the user prompt.",
120
+ )
121
+ tool_format_information_for_user_prompt: str = Field(
122
+ default="",
123
+ description="Format information that will be included in the user prompt to guide response formatting.",
124
+ )
125
+
126
+ poll_interval: float = Field(
127
+ default=1.0,
128
+ description="Time interval in seconds between polling attempts when waiting for sub-agent response.",
129
+ )
130
+ max_wait: float = Field(
131
+ default=120.0,
132
+ description="Maximum time in seconds to wait for the sub-agent response before timing out.",
133
+ )
134
+ stop_condition: Literal["stoppedStreamingAt", "completedAt"] = Field(
135
+ default="completedAt",
136
+ description="The condition that will be used to stop the polling for the sub-agent response.",
137
+ )
138
+
139
+ tool_input_json_schema: str | None = Field(
140
+ default=None,
141
+ description="A custom JSON schema to send to the llm as the tool input schema.",
142
+ )
143
+ returns_content_chunks: bool = Field(
144
+ default=False,
145
+ description="If set, the sub-agent response will be interpreted as a list of content chunks.",
146
+ )
147
+
148
+ system_reminders_config: list[
149
+ Annotated[
150
+ FixedSystemReminderConfig
151
+ | RegExpDetectedSystemReminderConfig
152
+ | ReferenceSystemReminderConfig,
153
+ Field(discriminator="type"),
154
+ ]
155
+ ] = Field(
156
+ default=[],
157
+ description="Configuration for the system reminders to add to the tool response.",
158
+ )
@@ -0,0 +1,393 @@
1
+ import asyncio
2
+ import contextlib
3
+ import json
4
+ import logging
5
+ import re
6
+ from datetime import datetime
7
+ from typing import cast, override
8
+
9
+ import unique_sdk
10
+ from pydantic import Field, TypeAdapter, create_model
11
+ from unique_sdk.utils.chat_in_space import send_message_and_wait_for_completion
12
+
13
+ from unique_toolkit._common.referencing import (
14
+ get_all_ref_numbers,
15
+ remove_all_refs,
16
+ replace_ref_number,
17
+ )
18
+ from unique_toolkit._common.utils.jinja.render import render_template
19
+ from unique_toolkit.agentic.evaluation.schemas import EvaluationMetricName
20
+ from unique_toolkit.agentic.tools.a2a.response_watcher import SubAgentResponseWatcher
21
+ from unique_toolkit.agentic.tools.a2a.tool._memory import (
22
+ get_sub_agent_short_term_memory_manager,
23
+ )
24
+ from unique_toolkit.agentic.tools.a2a.tool._schema import (
25
+ SubAgentShortTermMemorySchema,
26
+ SubAgentToolInput,
27
+ )
28
+ from unique_toolkit.agentic.tools.a2a.tool.config import (
29
+ RegExpDetectedSystemReminderConfig,
30
+ SubAgentSystemReminderType,
31
+ SubAgentToolConfig,
32
+ )
33
+ from unique_toolkit.agentic.tools.factory import ToolFactory
34
+ from unique_toolkit.agentic.tools.schemas import ToolCallResponse
35
+ from unique_toolkit.agentic.tools.tool import Tool
36
+ from unique_toolkit.agentic.tools.tool_progress_reporter import (
37
+ ProgressState,
38
+ ToolProgressReporter,
39
+ )
40
+ from unique_toolkit.app import ChatEvent
41
+ from unique_toolkit.content import ContentChunk
42
+ from unique_toolkit.language_model import (
43
+ LanguageModelFunction,
44
+ LanguageModelToolDescription,
45
+ )
46
+
47
+ logger = logging.getLogger(__name__)
48
+
49
+ _ContentChunkList = TypeAdapter(list[ContentChunk])
50
+
51
+
52
+ class SubAgentTool(Tool[SubAgentToolConfig]):
53
+ name: str = "SubAgentTool"
54
+
55
+ def __init__(
56
+ self,
57
+ configuration: SubAgentToolConfig,
58
+ event: ChatEvent,
59
+ tool_progress_reporter: ToolProgressReporter | None = None,
60
+ name: str = "SubAgentTool",
61
+ display_name: str = "SubAgentTool",
62
+ response_watcher: SubAgentResponseWatcher | None = None,
63
+ ):
64
+ super().__init__(configuration, event, tool_progress_reporter)
65
+ self._user_id = event.user_id
66
+ self._company_id = event.company_id
67
+
68
+ self.name = name
69
+ self._display_name = display_name
70
+
71
+ self._short_term_memory_manager = get_sub_agent_short_term_memory_manager(
72
+ company_id=self._company_id,
73
+ user_id=self._user_id,
74
+ chat_id=event.payload.chat_id,
75
+ assistant_id=self.config.assistant_id,
76
+ )
77
+ self._should_run_evaluation = False
78
+
79
+ self._response_watcher = response_watcher
80
+
81
+ # Synchronization state
82
+ self._sequence_number = 1
83
+ self._lock = asyncio.Lock()
84
+
85
+ @staticmethod
86
+ def get_sub_agent_reference_format(
87
+ name: str, sequence_number: int, reference_number: int
88
+ ) -> str:
89
+ return f"<sup><name>{name} {sequence_number}</name>{reference_number}</sup>"
90
+
91
+ @staticmethod
92
+ def get_sub_agent_reference_re(
93
+ name: str, sequence_number: int, reference_number: int
94
+ ) -> str:
95
+ return rf"<sup>\s*<name>\s*{re.escape(name)}\s*{sequence_number}\s*</name>\s*{reference_number}\s*</sup>"
96
+
97
+ @override
98
+ def display_name(self) -> str:
99
+ return self._display_name
100
+
101
+ @override
102
+ def tool_description(self) -> LanguageModelToolDescription:
103
+ if self.config.tool_input_json_schema is not None:
104
+ return LanguageModelToolDescription(
105
+ name=self.name,
106
+ description=self.config.tool_description,
107
+ parameters=json.loads(self.config.tool_input_json_schema),
108
+ )
109
+
110
+ tool_input_model_with_description = create_model(
111
+ "SubAgentToolInput",
112
+ user_message=(
113
+ str,
114
+ Field(description=self.config.param_description_sub_agent_user_message),
115
+ ),
116
+ )
117
+
118
+ return LanguageModelToolDescription(
119
+ name=self.name,
120
+ description=self.config.tool_description,
121
+ parameters=tool_input_model_with_description,
122
+ )
123
+
124
+ @override
125
+ def tool_description_for_system_prompt(self) -> str:
126
+ return self.config.tool_description_for_system_prompt
127
+
128
+ @override
129
+ def tool_format_information_for_system_prompt(self) -> str:
130
+ return self.config.tool_format_information_for_system_prompt
131
+
132
+ @override
133
+ def tool_description_for_user_prompt(self) -> str:
134
+ return self.config.tool_description_for_user_prompt
135
+
136
+ @override
137
+ def tool_format_information_for_user_prompt(self) -> str:
138
+ return self.config.tool_format_information_for_user_prompt
139
+
140
+ @override
141
+ def evaluation_check_list(self) -> list[EvaluationMetricName]:
142
+ return [EvaluationMetricName.SUB_AGENT] if self._should_run_evaluation else []
143
+
144
+ @override
145
+ def get_evaluation_checks_based_on_tool_response(
146
+ self,
147
+ tool_response: ToolCallResponse,
148
+ ) -> list[EvaluationMetricName]:
149
+ return []
150
+
151
+ @override
152
+ async def run(self, tool_call: LanguageModelFunction) -> ToolCallResponse:
153
+ if self.config.tool_input_json_schema is not None:
154
+ tool_input = json.dumps(tool_call.arguments)
155
+ else:
156
+ tool_input = SubAgentToolInput.model_validate(
157
+ tool_call.arguments
158
+ ).user_message
159
+
160
+ timestamp = datetime.now()
161
+
162
+ if self._lock.locked():
163
+ await self._notify_progress(
164
+ tool_call=tool_call,
165
+ message=f"Waiting for another run of `{self.display_name()}` to finish",
166
+ state=ProgressState.STARTED,
167
+ )
168
+
169
+ # When reusing the chat id, executing the sub agent in parrallel leads to race conditions and undefined behavior.
170
+ # To avoid this, we use a lock to serialize the execution of the same sub agent.
171
+ context = self._lock if self.config.reuse_chat else contextlib.nullcontext()
172
+
173
+ async with context:
174
+ sequence_number = self._sequence_number
175
+ self._sequence_number += 1
176
+
177
+ await self._notify_progress(
178
+ tool_call=tool_call,
179
+ message=tool_input,
180
+ state=ProgressState.RUNNING,
181
+ )
182
+
183
+ # Check if there is a saved chat id in short term memory
184
+ chat_id = await self._get_chat_id()
185
+
186
+ response = await self._execute_and_handle_timeout(
187
+ tool_user_message=tool_input,
188
+ chat_id=chat_id,
189
+ tool_call=tool_call,
190
+ )
191
+
192
+ self._should_run_evaluation |= (
193
+ response["assessment"] is not None and len(response["assessment"]) > 0
194
+ ) # Run evaluation if any sub agent returned an assessment
195
+
196
+ self._notify_watcher(response, sequence_number, timestamp)
197
+
198
+ if chat_id is None:
199
+ await self._save_chat_id(response["chatId"])
200
+
201
+ if response["text"] is None:
202
+ raise ValueError("No response returned from sub agent")
203
+
204
+ if self.config.returns_content_chunks:
205
+ content = ""
206
+ content_chunks = _ContentChunkList.validate_json(response["text"])
207
+ else:
208
+ content = self._prepare_response_references(
209
+ response=response["text"],
210
+ sequence_number=sequence_number,
211
+ )
212
+ content_chunks = None
213
+
214
+ await self._notify_progress(
215
+ tool_call=tool_call,
216
+ message=tool_input,
217
+ state=ProgressState.FINISHED,
218
+ )
219
+
220
+ return ToolCallResponse(
221
+ id=tool_call.id,
222
+ name=tool_call.name,
223
+ content=_format_response(
224
+ tool_name=self.name,
225
+ text=content,
226
+ system_reminders=self._get_system_reminders(response),
227
+ ),
228
+ content_chunks=content_chunks,
229
+ )
230
+
231
+ def _get_system_reminders(self, message: unique_sdk.Space.Message) -> list[str]:
232
+ reminders = []
233
+ for reminder_config in self.config.system_reminders_config:
234
+ if reminder_config.type == SubAgentSystemReminderType.FIXED:
235
+ reminders.append(
236
+ render_template(
237
+ reminder_config.reminder,
238
+ display_name=self.display_name(),
239
+ tool_name=self.name,
240
+ )
241
+ )
242
+ elif (
243
+ reminder_config.type == SubAgentSystemReminderType.REFERENCE
244
+ and self.config.use_sub_agent_references
245
+ and message["references"] is not None
246
+ and len(message["references"]) > 0
247
+ ):
248
+ reminders.append(
249
+ render_template(
250
+ reminder_config.reminder,
251
+ display_name=self.display_name(),
252
+ tool_name=self.name,
253
+ )
254
+ )
255
+ elif (
256
+ reminder_config.type == SubAgentSystemReminderType.REGEXP
257
+ and message["text"] is not None
258
+ ):
259
+ reminder_config = cast(
260
+ RegExpDetectedSystemReminderConfig, reminder_config
261
+ )
262
+ text_matches = [
263
+ match.group(0)
264
+ for match in reminder_config.regexp.finditer(message["text"])
265
+ ]
266
+ if len(text_matches) > 0:
267
+ reminders.append(
268
+ render_template(
269
+ reminder_config.reminder,
270
+ display_name=self.display_name(),
271
+ tool_name=self.name,
272
+ text_matches=text_matches,
273
+ )
274
+ )
275
+
276
+ return reminders
277
+
278
+ async def _get_chat_id(self) -> str | None:
279
+ if not self.config.reuse_chat:
280
+ return None
281
+
282
+ if self.config.chat_id is not None:
283
+ return self.config.chat_id
284
+
285
+ # Check if there is a saved chat id in short term memory
286
+ short_term_memory = await self._short_term_memory_manager.load_async()
287
+
288
+ if short_term_memory is not None:
289
+ return short_term_memory.chat_id
290
+
291
+ return None
292
+
293
+ def _prepare_response_references(self, response: str, sequence_number: int) -> str:
294
+ if not self.config.use_sub_agent_references:
295
+ # Remove all references from the response
296
+ response = remove_all_refs(response)
297
+ return response
298
+
299
+ for ref_number in get_all_ref_numbers(response):
300
+ reference = self.get_sub_agent_reference_format(
301
+ name=self.name,
302
+ sequence_number=sequence_number,
303
+ reference_number=ref_number,
304
+ )
305
+ response = replace_ref_number(
306
+ text=response, ref_number=ref_number, replacement=reference
307
+ )
308
+ return response
309
+
310
+ async def _save_chat_id(self, chat_id: str) -> None:
311
+ if not self.config.reuse_chat:
312
+ return
313
+
314
+ await self._short_term_memory_manager.save_async(
315
+ SubAgentShortTermMemorySchema(chat_id=chat_id)
316
+ )
317
+
318
+ async def _notify_progress(
319
+ self,
320
+ tool_call: LanguageModelFunction,
321
+ message: str,
322
+ state: ProgressState,
323
+ ) -> None:
324
+ if self.tool_progress_reporter is not None:
325
+ await self.tool_progress_reporter.notify_from_tool_call(
326
+ tool_call=tool_call,
327
+ name=self._display_name,
328
+ message=message,
329
+ state=state,
330
+ )
331
+
332
+ def _notify_watcher(
333
+ self,
334
+ response: unique_sdk.Space.Message,
335
+ sequence_number: int,
336
+ timestamp: datetime,
337
+ ) -> None:
338
+ if self._response_watcher is not None:
339
+ self._response_watcher.notify_response(
340
+ assistant_id=self.config.assistant_id,
341
+ name=self.name,
342
+ sequence_number=sequence_number,
343
+ response=response,
344
+ timestamp=timestamp,
345
+ )
346
+ else:
347
+ logger.warning(
348
+ "No response watcher found for sub agent %s (assistant_id: %s)",
349
+ self.name,
350
+ self.config.assistant_id,
351
+ )
352
+
353
+ async def _execute_and_handle_timeout(
354
+ self,
355
+ tool_user_message: str,
356
+ chat_id: str | None,
357
+ tool_call: LanguageModelFunction,
358
+ ) -> unique_sdk.Space.Message:
359
+ try:
360
+ return await send_message_and_wait_for_completion(
361
+ user_id=self._user_id,
362
+ assistant_id=self.config.assistant_id,
363
+ company_id=self._company_id,
364
+ text=tool_user_message,
365
+ chat_id=chat_id,
366
+ poll_interval=self.config.poll_interval,
367
+ tool_choices=self.config.forced_tools,
368
+ max_wait=self.config.max_wait,
369
+ stop_condition=self.config.stop_condition,
370
+ )
371
+ except TimeoutError as e:
372
+ await self._notify_progress(
373
+ tool_call=tool_call,
374
+ message="Timeout while waiting for response from sub agent.",
375
+ state=ProgressState.FAILED,
376
+ )
377
+
378
+ raise TimeoutError(
379
+ "Timeout while waiting for response from sub agent. The user should consider increasing the max wait time.",
380
+ ) from e
381
+
382
+
383
+ def _format_response(tool_name: str, text: str, system_reminders: list[str]) -> str:
384
+ if len(system_reminders) == 0:
385
+ return text
386
+
387
+ reponse_key = f"{tool_name} response"
388
+ response = {reponse_key: text, "SYSTEM_REMINDERS": system_reminders}
389
+
390
+ return json.dumps(response, indent=2)
391
+
392
+
393
+ ToolFactory.register_tool(SubAgentTool, SubAgentToolConfig)