unique_toolkit 0.7.7__py3-none-any.whl → 1.23.0__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.

Potentially problematic release.


This version of unique_toolkit might be problematic. Click here for more details.

Files changed (166) hide show
  1. unique_toolkit/__init__.py +28 -1
  2. unique_toolkit/_common/api_calling/human_verification_manager.py +343 -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 +252 -0
  14. unique_toolkit/_common/docx_generator/template/Doc Template.docx +0 -0
  15. unique_toolkit/_common/endpoint_builder.py +305 -0
  16. unique_toolkit/_common/endpoint_requestor.py +430 -0
  17. unique_toolkit/_common/exception.py +24 -0
  18. unique_toolkit/_common/feature_flags/schema.py +9 -0
  19. unique_toolkit/_common/pydantic/rjsf_tags.py +936 -0
  20. unique_toolkit/_common/pydantic_helpers.py +154 -0
  21. unique_toolkit/_common/referencing.py +53 -0
  22. unique_toolkit/_common/string_utilities.py +140 -0
  23. unique_toolkit/_common/tests/test_referencing.py +521 -0
  24. unique_toolkit/_common/tests/test_string_utilities.py +506 -0
  25. unique_toolkit/_common/token/image_token_counting.py +67 -0
  26. unique_toolkit/_common/token/token_counting.py +204 -0
  27. unique_toolkit/_common/utils/__init__.py +1 -0
  28. unique_toolkit/_common/utils/files.py +43 -0
  29. unique_toolkit/_common/utils/structured_output/__init__.py +1 -0
  30. unique_toolkit/_common/utils/structured_output/schema.py +5 -0
  31. unique_toolkit/_common/utils/write_configuration.py +51 -0
  32. unique_toolkit/_common/validators.py +101 -4
  33. unique_toolkit/agentic/__init__.py +1 -0
  34. unique_toolkit/agentic/debug_info_manager/debug_info_manager.py +28 -0
  35. unique_toolkit/agentic/debug_info_manager/test/test_debug_info_manager.py +278 -0
  36. unique_toolkit/agentic/evaluation/config.py +36 -0
  37. unique_toolkit/{evaluators → agentic/evaluation}/context_relevancy/prompts.py +25 -0
  38. unique_toolkit/agentic/evaluation/context_relevancy/schema.py +80 -0
  39. unique_toolkit/agentic/evaluation/context_relevancy/service.py +273 -0
  40. unique_toolkit/agentic/evaluation/evaluation_manager.py +218 -0
  41. unique_toolkit/agentic/evaluation/hallucination/constants.py +61 -0
  42. unique_toolkit/agentic/evaluation/hallucination/hallucination_evaluation.py +111 -0
  43. unique_toolkit/{evaluators → agentic/evaluation}/hallucination/prompts.py +1 -1
  44. unique_toolkit/{evaluators → agentic/evaluation}/hallucination/service.py +16 -15
  45. unique_toolkit/{evaluators → agentic/evaluation}/hallucination/utils.py +30 -20
  46. unique_toolkit/{evaluators → agentic/evaluation}/output_parser.py +20 -2
  47. unique_toolkit/{evaluators → agentic/evaluation}/schemas.py +27 -7
  48. unique_toolkit/agentic/evaluation/tests/test_context_relevancy_service.py +253 -0
  49. unique_toolkit/agentic/evaluation/tests/test_output_parser.py +87 -0
  50. unique_toolkit/agentic/history_manager/history_construction_with_contents.py +297 -0
  51. unique_toolkit/agentic/history_manager/history_manager.py +242 -0
  52. unique_toolkit/agentic/history_manager/loop_token_reducer.py +484 -0
  53. unique_toolkit/agentic/history_manager/utils.py +96 -0
  54. unique_toolkit/agentic/postprocessor/postprocessor_manager.py +212 -0
  55. unique_toolkit/agentic/reference_manager/reference_manager.py +103 -0
  56. unique_toolkit/agentic/responses_api/__init__.py +19 -0
  57. unique_toolkit/agentic/responses_api/postprocessors/code_display.py +63 -0
  58. unique_toolkit/agentic/responses_api/postprocessors/generated_files.py +145 -0
  59. unique_toolkit/agentic/responses_api/stream_handler.py +15 -0
  60. unique_toolkit/agentic/short_term_memory_manager/persistent_short_term_memory_manager.py +141 -0
  61. unique_toolkit/agentic/thinking_manager/thinking_manager.py +103 -0
  62. unique_toolkit/agentic/tools/__init__.py +1 -0
  63. unique_toolkit/agentic/tools/a2a/__init__.py +36 -0
  64. unique_toolkit/agentic/tools/a2a/config.py +17 -0
  65. unique_toolkit/agentic/tools/a2a/evaluation/__init__.py +15 -0
  66. unique_toolkit/agentic/tools/a2a/evaluation/_utils.py +66 -0
  67. unique_toolkit/agentic/tools/a2a/evaluation/config.py +55 -0
  68. unique_toolkit/agentic/tools/a2a/evaluation/evaluator.py +260 -0
  69. unique_toolkit/agentic/tools/a2a/evaluation/summarization_user_message.j2 +9 -0
  70. unique_toolkit/agentic/tools/a2a/manager.py +55 -0
  71. unique_toolkit/agentic/tools/a2a/postprocessing/__init__.py +21 -0
  72. unique_toolkit/agentic/tools/a2a/postprocessing/_display_utils.py +185 -0
  73. unique_toolkit/agentic/tools/a2a/postprocessing/_ref_utils.py +73 -0
  74. unique_toolkit/agentic/tools/a2a/postprocessing/config.py +45 -0
  75. unique_toolkit/agentic/tools/a2a/postprocessing/display.py +180 -0
  76. unique_toolkit/agentic/tools/a2a/postprocessing/references.py +101 -0
  77. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display_utils.py +1335 -0
  78. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_ref_utils.py +603 -0
  79. unique_toolkit/agentic/tools/a2a/prompts.py +46 -0
  80. unique_toolkit/agentic/tools/a2a/response_watcher/__init__.py +6 -0
  81. unique_toolkit/agentic/tools/a2a/response_watcher/service.py +91 -0
  82. unique_toolkit/agentic/tools/a2a/tool/__init__.py +4 -0
  83. unique_toolkit/agentic/tools/a2a/tool/_memory.py +26 -0
  84. unique_toolkit/agentic/tools/a2a/tool/_schema.py +9 -0
  85. unique_toolkit/agentic/tools/a2a/tool/config.py +73 -0
  86. unique_toolkit/agentic/tools/a2a/tool/service.py +306 -0
  87. unique_toolkit/agentic/tools/agent_chunks_hanlder.py +65 -0
  88. unique_toolkit/agentic/tools/config.py +167 -0
  89. unique_toolkit/agentic/tools/factory.py +44 -0
  90. unique_toolkit/agentic/tools/mcp/__init__.py +4 -0
  91. unique_toolkit/agentic/tools/mcp/manager.py +71 -0
  92. unique_toolkit/agentic/tools/mcp/models.py +28 -0
  93. unique_toolkit/agentic/tools/mcp/tool_wrapper.py +234 -0
  94. unique_toolkit/agentic/tools/openai_builtin/__init__.py +11 -0
  95. unique_toolkit/agentic/tools/openai_builtin/base.py +30 -0
  96. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/__init__.py +8 -0
  97. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/config.py +57 -0
  98. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/service.py +230 -0
  99. unique_toolkit/agentic/tools/openai_builtin/manager.py +62 -0
  100. unique_toolkit/agentic/tools/schemas.py +141 -0
  101. unique_toolkit/agentic/tools/test/test_mcp_manager.py +536 -0
  102. unique_toolkit/agentic/tools/test/test_tool_progress_reporter.py +445 -0
  103. unique_toolkit/agentic/tools/tool.py +183 -0
  104. unique_toolkit/agentic/tools/tool_manager.py +523 -0
  105. unique_toolkit/agentic/tools/tool_progress_reporter.py +285 -0
  106. unique_toolkit/agentic/tools/utils/__init__.py +19 -0
  107. unique_toolkit/agentic/tools/utils/execution/__init__.py +1 -0
  108. unique_toolkit/agentic/tools/utils/execution/execution.py +286 -0
  109. unique_toolkit/agentic/tools/utils/source_handling/__init__.py +0 -0
  110. unique_toolkit/agentic/tools/utils/source_handling/schema.py +21 -0
  111. unique_toolkit/agentic/tools/utils/source_handling/source_formatting.py +207 -0
  112. unique_toolkit/agentic/tools/utils/source_handling/tests/test_source_formatting.py +216 -0
  113. unique_toolkit/app/__init__.py +6 -0
  114. unique_toolkit/app/dev_util.py +180 -0
  115. unique_toolkit/app/init_sdk.py +32 -1
  116. unique_toolkit/app/schemas.py +198 -31
  117. unique_toolkit/app/unique_settings.py +367 -0
  118. unique_toolkit/chat/__init__.py +8 -1
  119. unique_toolkit/chat/deprecated/service.py +232 -0
  120. unique_toolkit/chat/functions.py +642 -77
  121. unique_toolkit/chat/rendering.py +34 -0
  122. unique_toolkit/chat/responses_api.py +461 -0
  123. unique_toolkit/chat/schemas.py +133 -2
  124. unique_toolkit/chat/service.py +115 -767
  125. unique_toolkit/content/functions.py +153 -4
  126. unique_toolkit/content/schemas.py +122 -15
  127. unique_toolkit/content/service.py +278 -44
  128. unique_toolkit/content/smart_rules.py +301 -0
  129. unique_toolkit/content/utils.py +8 -3
  130. unique_toolkit/embedding/service.py +102 -11
  131. unique_toolkit/framework_utilities/__init__.py +1 -0
  132. unique_toolkit/framework_utilities/langchain/client.py +71 -0
  133. unique_toolkit/framework_utilities/langchain/history.py +19 -0
  134. unique_toolkit/framework_utilities/openai/__init__.py +6 -0
  135. unique_toolkit/framework_utilities/openai/client.py +83 -0
  136. unique_toolkit/framework_utilities/openai/message_builder.py +229 -0
  137. unique_toolkit/framework_utilities/utils.py +23 -0
  138. unique_toolkit/language_model/__init__.py +3 -0
  139. unique_toolkit/language_model/builder.py +27 -11
  140. unique_toolkit/language_model/default_language_model.py +3 -0
  141. unique_toolkit/language_model/functions.py +327 -43
  142. unique_toolkit/language_model/infos.py +992 -50
  143. unique_toolkit/language_model/reference.py +242 -0
  144. unique_toolkit/language_model/schemas.py +475 -48
  145. unique_toolkit/language_model/service.py +228 -27
  146. unique_toolkit/protocols/support.py +145 -0
  147. unique_toolkit/services/__init__.py +7 -0
  148. unique_toolkit/services/chat_service.py +1630 -0
  149. unique_toolkit/services/knowledge_base.py +861 -0
  150. unique_toolkit/short_term_memory/service.py +178 -41
  151. unique_toolkit/smart_rules/__init__.py +0 -0
  152. unique_toolkit/smart_rules/compile.py +56 -0
  153. unique_toolkit/test_utilities/events.py +197 -0
  154. {unique_toolkit-0.7.7.dist-info → unique_toolkit-1.23.0.dist-info}/METADATA +606 -7
  155. unique_toolkit-1.23.0.dist-info/RECORD +182 -0
  156. unique_toolkit/evaluators/__init__.py +0 -1
  157. unique_toolkit/evaluators/config.py +0 -35
  158. unique_toolkit/evaluators/constants.py +0 -1
  159. unique_toolkit/evaluators/context_relevancy/constants.py +0 -32
  160. unique_toolkit/evaluators/context_relevancy/service.py +0 -53
  161. unique_toolkit/evaluators/context_relevancy/utils.py +0 -142
  162. unique_toolkit/evaluators/hallucination/constants.py +0 -41
  163. unique_toolkit-0.7.7.dist-info/RECORD +0 -64
  164. /unique_toolkit/{evaluators → agentic/evaluation}/exception.py +0 -0
  165. {unique_toolkit-0.7.7.dist-info → unique_toolkit-1.23.0.dist-info}/LICENSE +0 -0
  166. {unique_toolkit-0.7.7.dist-info → unique_toolkit-1.23.0.dist-info}/WHEEL +0 -0
@@ -2,16 +2,43 @@
2
2
  from unique_toolkit.chat import ChatService
3
3
  from unique_toolkit.content import ContentService
4
4
  from unique_toolkit.embedding import EmbeddingService
5
- from unique_toolkit.language_model import LanguageModelMessages, LanguageModelService
5
+ from unique_toolkit.framework_utilities.openai.client import (
6
+ get_async_openai_client,
7
+ get_openai_client,
8
+ )
9
+ from unique_toolkit.language_model import (
10
+ LanguageModelMessages,
11
+ LanguageModelName,
12
+ LanguageModelService,
13
+ LanguageModelToolDescription,
14
+ )
15
+ from unique_toolkit.services.knowledge_base import KnowledgeBaseService
6
16
  from unique_toolkit.short_term_memory import ShortTermMemoryService
7
17
 
18
+ # Conditionally import langchain utilities if langchain is installed
19
+ try:
20
+ from unique_toolkit.framework_utilities.langchain.client import get_langchain_client # noqa: F401, I001
21
+
22
+ _LANGCHAIN_AVAILABLE = True
23
+ except ImportError:
24
+ _LANGCHAIN_AVAILABLE = False
25
+
8
26
  # You can add other classes you frequently use here as well
9
27
 
10
28
  __all__ = [
11
29
  "LanguageModelService",
12
30
  "LanguageModelMessages",
31
+ "LanguageModelName",
32
+ "LanguageModelToolDescription",
13
33
  "ChatService",
14
34
  "ContentService",
15
35
  "EmbeddingService",
16
36
  "ShortTermMemoryService",
37
+ "KnowledgeBaseService",
38
+ "get_openai_client",
39
+ "get_async_openai_client",
17
40
  ]
41
+
42
+ # Add langchain-specific exports if available
43
+ if _LANGCHAIN_AVAILABLE:
44
+ __all__.append("get_langchain_client")
@@ -0,0 +1,343 @@
1
+ import hashlib
2
+ from datetime import datetime
3
+ from logging import Logger
4
+ from typing import Any, Generic
5
+
6
+ import jinja2
7
+ from pydantic import BaseModel, ValidationError
8
+
9
+ from unique_toolkit._common.endpoint_builder import (
10
+ ApiOperationProtocol,
11
+ PathParamsSpec,
12
+ PathParamsType,
13
+ PayloadParamSpec,
14
+ PayloadType,
15
+ ResponseType,
16
+ )
17
+ from unique_toolkit._common.endpoint_requestor import (
18
+ RequestContext,
19
+ RequestorType,
20
+ build_requestor,
21
+ )
22
+ from unique_toolkit._common.pydantic_helpers import (
23
+ create_complement_model,
24
+ create_union_model,
25
+ )
26
+ from unique_toolkit._common.string_utilities import (
27
+ dict_to_markdown_table,
28
+ extract_dicts_from_string,
29
+ )
30
+ from unique_toolkit.chat.schemas import ChatMessage, ChatMessageRole
31
+
32
+
33
+ class HumanConfirmation(BaseModel):
34
+ payload_hash: str
35
+ time_stamp: datetime
36
+
37
+
38
+ NEXT_USER_MESSAGE_JINJA2_TEMPLATE = jinja2.Template("""I confirm the api call with the following data:
39
+ ```json
40
+ {{ api_call_as_json }}
41
+ ```""")
42
+
43
+
44
+ ASSISTANT_CONFIRMATION_MESSAGE_JINJA2_TEMPLATE = jinja2.Template(
45
+ """
46
+ \n
47
+ {{ api_call_as_markdown_table }}
48
+ \n\n
49
+ [{{ button_text }}](https://prompt={{ next_user_message | urlencode }})"""
50
+ )
51
+
52
+
53
+ class HumanVerificationManagerForApiCalling(
54
+ Generic[
55
+ PathParamsSpec,
56
+ PathParamsType,
57
+ PayloadParamSpec,
58
+ PayloadType,
59
+ ResponseType,
60
+ ]
61
+ ):
62
+ """
63
+ Manages human verification for api calling.
64
+
65
+ The idea is that the manager is able to produce the verification message to the user
66
+ and to detect an api call from the user message.
67
+
68
+ If it detects such a verification message in the user message, it will call the api
69
+ and incorporate the response into the user message.
70
+ """
71
+
72
+ def __init__(
73
+ self,
74
+ *,
75
+ logger: Logger,
76
+ operation: type[
77
+ ApiOperationProtocol[
78
+ PathParamsSpec,
79
+ PathParamsType,
80
+ PayloadParamSpec,
81
+ PayloadType,
82
+ ResponseType,
83
+ ]
84
+ ],
85
+ requestor_type: RequestorType = RequestorType.REQUESTS,
86
+ environment_payload_params: BaseModel | None = None,
87
+ modifiable_payload_params_model: type[BaseModel] | None = None,
88
+ **kwargs: dict[str, Any],
89
+ ):
90
+ """
91
+ Manages human verification for api calling.
92
+
93
+ Args:
94
+ logger: The logger to use for logging.
95
+ operation: The operation to use for the api calling.
96
+ requestor_type: The requestor type to use for the api calling.
97
+ environment_payload_params: The environment payload params to use for the api calling.
98
+ If None, the modifiable params model will be the operation payload model.
99
+ This can be useful for parameters in the payload that should not be modified by the user.
100
+ modifiable_payload_params_model: The modifiable payload params model to use for the api calling.
101
+ If None, a complement model will be created using the operation payload model
102
+ and the environment payload params.
103
+ If provided, it will be used instead of the complement model.
104
+ This is necessary if the modifiable params model is required
105
+ to use custom validators or serializers.
106
+ **kwargs: Additional keyword arguments to pass to the requestor.
107
+ """
108
+ self._logger = logger
109
+ self._operation = operation
110
+ self._environment_payload_params = environment_payload_params
111
+ # Create internal models for this manager instance
112
+
113
+ if self._environment_payload_params is None:
114
+ self._modifiable_payload_params_model = self._operation.payload_model()
115
+ else:
116
+ if modifiable_payload_params_model is None:
117
+ self._modifiable_payload_params_model = create_complement_model(
118
+ model_type_a=self._operation.payload_model(),
119
+ model_type_b=type(self._environment_payload_params),
120
+ )
121
+ else:
122
+ # This is necessary if the modifiable params model is required
123
+ # to use custom validators or serializers.
124
+ self._modifiable_payload_params_model = modifiable_payload_params_model
125
+
126
+ if self._environment_payload_params is not None:
127
+ combined_keys = set(
128
+ self._modifiable_payload_params_model.model_fields.keys()
129
+ ) | set(type(self._environment_payload_params).model_fields.keys())
130
+ payload_keys = set(self._operation.payload_model().model_fields.keys())
131
+ if not payload_keys.issubset(combined_keys):
132
+ raise ValueError(
133
+ "The modifiable params model + the environment parameters do not have all the keys of the operation payload model."
134
+ )
135
+
136
+ class VerificationModel(BaseModel):
137
+ confirmation: HumanConfirmation
138
+ modifiable_params: self._modifiable_payload_params_model # type: ignore
139
+
140
+ self._verification_model = VerificationModel
141
+
142
+ self._requestor_type = requestor_type
143
+ self._combined_params_model = create_union_model(
144
+ model_type_a=self._operation.path_params_model(),
145
+ model_type_b=self._operation.payload_model(),
146
+ )
147
+ self._requestor = build_requestor(
148
+ requestor_type=requestor_type,
149
+ operation_type=operation,
150
+ combined_model=self._combined_params_model,
151
+ **kwargs,
152
+ )
153
+
154
+ def detect_api_calls_from_user_message(
155
+ self,
156
+ *,
157
+ last_assistant_message: ChatMessage,
158
+ user_message: str,
159
+ ) -> PayloadType | None:
160
+ user_message_dicts = extract_dicts_from_string(user_message)
161
+ if len(user_message_dicts) == 0:
162
+ return None
163
+
164
+ user_message_dicts.reverse()
165
+ for user_message_dict in user_message_dicts:
166
+ try:
167
+ # Convert dict to payload model first, then create payload
168
+ verfication_data = self._verification_model.model_validate(
169
+ user_message_dict, by_alias=True, by_name=True
170
+ )
171
+ if self._verify_human_verification(
172
+ verfication_data.confirmation, last_assistant_message
173
+ ):
174
+ payload_dict = verfication_data.modifiable_params.model_dump()
175
+ if self._environment_payload_params is not None:
176
+ payload_dict.update(
177
+ self._environment_payload_params.model_dump()
178
+ )
179
+
180
+ return self._operation.payload_model().model_validate(
181
+ payload_dict, by_alias=True, by_name=True
182
+ )
183
+
184
+ except Exception as e:
185
+ self._logger.error(f"Error detecting api calls from user message: {e}")
186
+
187
+ return None
188
+
189
+ def _verify_human_verification(
190
+ self, confirmation: HumanConfirmation, last_assistant_message: ChatMessage
191
+ ) -> bool:
192
+ if (
193
+ last_assistant_message.role != ChatMessageRole.ASSISTANT
194
+ or last_assistant_message.content is None
195
+ ):
196
+ self._logger.error(
197
+ "Last assistant message is not an assistant message or content is empty."
198
+ )
199
+ return False
200
+
201
+ return confirmation.payload_hash in last_assistant_message.content
202
+
203
+ def _create_next_user_message(self, payload: PayloadType) -> str:
204
+ # Extract only the modifiable fields from the payload
205
+ payload_dict = payload.model_dump()
206
+ if self._environment_payload_params is not None:
207
+ # Remove environment params from payload to avoid validation errors
208
+ environment_fields = set(
209
+ type(self._environment_payload_params).model_fields.keys()
210
+ )
211
+ modifiable_dict = {
212
+ k: v for k, v in payload_dict.items() if k not in environment_fields
213
+ }
214
+ else:
215
+ modifiable_dict = payload_dict
216
+
217
+ modifiable_params = self._modifiable_payload_params_model.model_validate(
218
+ modifiable_dict,
219
+ by_alias=True,
220
+ by_name=True,
221
+ )
222
+ api_call = self._verification_model(
223
+ modifiable_params=modifiable_params,
224
+ confirmation=HumanConfirmation(
225
+ payload_hash=hashlib.sha256(
226
+ modifiable_params.model_dump_json().encode()
227
+ ).hexdigest(),
228
+ time_stamp=datetime.now(),
229
+ ),
230
+ )
231
+ return NEXT_USER_MESSAGE_JINJA2_TEMPLATE.render(
232
+ api_call_as_json=api_call.model_dump_json(indent=2)
233
+ )
234
+
235
+ def create_assistant_confirmation_message(
236
+ self, *, payload: PayloadType, button_text: str = "Confirm"
237
+ ) -> str:
238
+ return ASSISTANT_CONFIRMATION_MESSAGE_JINJA2_TEMPLATE.render(
239
+ api_call_as_markdown_table=dict_to_markdown_table(payload.model_dump()),
240
+ button_text=button_text,
241
+ next_user_message=self._create_next_user_message(payload),
242
+ )
243
+
244
+ def call_api(
245
+ self,
246
+ *,
247
+ context: RequestContext,
248
+ path_params: PathParamsType,
249
+ payload: PayloadType,
250
+ ) -> ResponseType:
251
+ """
252
+ Call the api with the given path params, payload and secured payload params.
253
+
254
+ The `secured payload params` are params that are enforced by the application.
255
+ It should generally be not possible for the user to adapt those but here we
256
+ ensure that the application has the last word.
257
+
258
+ """
259
+ params = path_params.model_dump()
260
+ params.update(payload.model_dump())
261
+
262
+ response = self._requestor.request(
263
+ context=context,
264
+ **params,
265
+ )
266
+ try:
267
+ return self._operation.handle_response(response)
268
+ except ValidationError as e:
269
+ self._logger.error(f"Error calling api: {e}. Response: {response}")
270
+ raise e
271
+
272
+
273
+ if __name__ == "__main__":
274
+ import logging
275
+ from string import Template
276
+
277
+ from unique_toolkit._common.endpoint_builder import (
278
+ EndpointMethods,
279
+ build_api_operation,
280
+ )
281
+
282
+ class GetUserPathParams(BaseModel):
283
+ user_id: int
284
+
285
+ class GetUserRequestBody(BaseModel):
286
+ include_profile: bool = False
287
+
288
+ class UserResponse(BaseModel):
289
+ id: int
290
+ name: str
291
+
292
+ class CombinedParams(GetUserPathParams, GetUserRequestBody):
293
+ pass
294
+
295
+ UserApiOperation = build_api_operation(
296
+ method=EndpointMethods.GET,
297
+ path_template=Template("/users/{user_id}"),
298
+ path_params_constructor=GetUserPathParams,
299
+ payload_constructor=GetUserRequestBody,
300
+ response_model_type=UserResponse,
301
+ )
302
+
303
+ human_verification_manager = HumanVerificationManagerForApiCalling(
304
+ logger=logging.getLogger(__name__),
305
+ operation=UserApiOperation,
306
+ requestor_type=RequestorType.FAKE,
307
+ return_value={"id": 100, "name": "John Doe"},
308
+ )
309
+
310
+ payload = GetUserRequestBody(include_profile=True)
311
+
312
+ api_call = human_verification_manager._verification_model(
313
+ modifiable_params=payload,
314
+ confirmation=HumanConfirmation(
315
+ payload_hash=hashlib.sha256(payload.model_dump_json().encode()).hexdigest(),
316
+ time_stamp=datetime.now(),
317
+ ),
318
+ )
319
+
320
+ last_assistant_message = ChatMessage(
321
+ role=ChatMessageRole.ASSISTANT,
322
+ text=api_call.confirmation.payload_hash,
323
+ chat_id="123",
324
+ )
325
+
326
+ user_message_with_api_call = human_verification_manager._create_next_user_message(
327
+ payload=payload
328
+ )
329
+
330
+ print(user_message_with_api_call)
331
+
332
+ payload = human_verification_manager.detect_api_calls_from_user_message(
333
+ user_message=user_message_with_api_call,
334
+ last_assistant_message=last_assistant_message,
335
+ )
336
+
337
+ if payload is None:
338
+ print("❌ Detection failed - payload is None")
339
+ exit(1)
340
+ else:
341
+ print("✅ Detection successful!")
342
+ print(f"Payload: {payload.model_dump()}")
343
+ print("✅ Dict extraction from string works correctly!")
@@ -0,0 +1,303 @@
1
+ """
2
+ The following can be used to define a pydantic BaseModel that has has
3
+ an attribute of type Pydantic BaseModel.
4
+
5
+ This is useful for:
6
+ - Tooldefinition for large language models (LLMs) with flexible parameters.
7
+ - General Endpoint defintions from configuration
8
+ """
9
+
10
+ import json
11
+ from enum import StrEnum
12
+ from typing import Annotated, Any, TypeVar, Union, get_args, get_origin
13
+
14
+ from jambo import SchemaConverter
15
+ from jambo.types.json_schema_type import JSONSchema
16
+ from pydantic import (
17
+ BaseModel,
18
+ BeforeValidator,
19
+ Field,
20
+ create_model,
21
+ )
22
+
23
+
24
+ def _get_actual_type(python_type: type) -> type | None | Any:
25
+ if get_origin(python_type) is not None:
26
+ origin = get_origin(python_type)
27
+ args = get_args(python_type)
28
+
29
+ if origin is Annotated:
30
+ # For Annotated types, the first argument is the actual type
31
+ if args:
32
+ actual_type = args[0]
33
+ # Recursively handle nested generic types (e.g., Annotated[Optional[str], ...])
34
+ if get_origin(actual_type) is not None:
35
+ return _get_actual_type(actual_type)
36
+ else:
37
+ raise ValueError(f"Invalid Annotated type: {python_type}")
38
+ elif origin is Union:
39
+ # For Union types (including Optional), use the first non-None type
40
+ if args:
41
+ for arg in args:
42
+ if arg is not type(None): # Skip NoneType
43
+ return _get_actual_type(arg)
44
+ raise ValueError(f"Union type contains only None: {python_type}")
45
+ else:
46
+ raise ValueError(f"Invalid Union type: {python_type}")
47
+ else:
48
+ # Other generic types, use the origin
49
+ actual_type = origin
50
+ else:
51
+ # Regular type
52
+ actual_type = python_type
53
+
54
+ return actual_type
55
+
56
+
57
+ class ParameterType(StrEnum):
58
+ STRING = "string"
59
+ INTEGER = "integer"
60
+ NUMBER = "number"
61
+ BOOLEAN = "boolean"
62
+
63
+ def to_python_type(self) -> type:
64
+ """Convert ParameterType to Python type"""
65
+
66
+ match self:
67
+ case ParameterType.STRING:
68
+ return str
69
+ case ParameterType.INTEGER:
70
+ return int
71
+ case ParameterType.NUMBER:
72
+ return float
73
+ case ParameterType.BOOLEAN:
74
+ return bool
75
+ case _:
76
+ raise ValueError(f"Invalid ParameterType: {self}")
77
+
78
+ @classmethod
79
+ def from_python_type(cls, python_type: type) -> "ParameterType":
80
+ type_to_check = _get_actual_type(python_type)
81
+
82
+ # Ensure we have a class before calling issubclass
83
+ if not isinstance(type_to_check, type):
84
+ raise ValueError(f"Invalid Python type: {python_type}")
85
+
86
+ # Check bool first since bool is a subclass of int in Python
87
+ if issubclass(type_to_check, bool):
88
+ return cls.BOOLEAN
89
+ if issubclass(type_to_check, int):
90
+ return cls.INTEGER
91
+ if issubclass(type_to_check, float):
92
+ return cls.NUMBER
93
+ if issubclass(type_to_check, str):
94
+ return cls.STRING
95
+ raise ValueError(f"Invalid Python type: {python_type}")
96
+
97
+
98
+ class Parameter(BaseModel):
99
+ type: ParameterType
100
+ name: str
101
+ description: str
102
+ required: bool
103
+
104
+
105
+ def create_pydantic_model_from_parameter_list(
106
+ title: str, parameter_list: list[Parameter]
107
+ ) -> type[BaseModel]:
108
+ """Create a Pydantic model from MCP tool's input schema"""
109
+
110
+ # Convert JSON schema properties to Pydantic fields
111
+ fields = {}
112
+ for parameter in parameter_list:
113
+ if parameter.required:
114
+ field = Field(description=parameter.description)
115
+ else:
116
+ field = Field(default=None, description=parameter.description)
117
+
118
+ fields[parameter.name] = (
119
+ parameter.type.to_python_type(),
120
+ field,
121
+ )
122
+
123
+ return create_model(title, **fields)
124
+
125
+
126
+ def convert_to_base_model_type(
127
+ value: type[BaseModel] | str | list[Parameter] | None,
128
+ ) -> type[BaseModel]:
129
+ """
130
+ BeforeValidator that ensures the final type is always of type[BaseModel].
131
+
132
+ If the input is already a BaseModel class, returns it as-is.
133
+ If the input is a list of Parameter as defined above, converts it to a BaseModel class
134
+ If the input is a str (JSON schema), converts it to a BaseModel class using SchemaConverter from Jambo.
135
+ """
136
+ if isinstance(value, type) and issubclass(value, BaseModel):
137
+ return value
138
+
139
+ if isinstance(value, list):
140
+ if all(isinstance(item, Parameter) for item in value):
141
+ return create_pydantic_model_from_parameter_list("Parameters", value)
142
+
143
+ converter = SchemaConverter()
144
+ if isinstance(value, str):
145
+ return converter.build(JSONSchema(**json.loads(value)))
146
+
147
+ raise ValueError(f"Invalid value: {value}")
148
+
149
+
150
+ def base_model_to_parameter_list(model: type[BaseModel]) -> list[Parameter]:
151
+ parameter = []
152
+ for field_name, field_info in model.model_fields.items():
153
+ parameter.append(
154
+ Parameter(
155
+ type=ParameterType.from_python_type(field_info.annotation or str),
156
+ name=field_name,
157
+ description=field_info.description or "",
158
+ required=field_info.is_required(),
159
+ )
160
+ )
161
+ return parameter
162
+
163
+
164
+ # Create the annotated type that ensures BaseModel and generates clean JSON schema
165
+
166
+ TModel = TypeVar("TModel", bound=BaseModel)
167
+
168
+
169
+ class BaseModelTypeTitle(StrEnum):
170
+ LIST_OF_PARAMETERS = "List of Parameters"
171
+ JSON_SCHEMA_AS_STRING = "JSON Schema as String"
172
+ USE_MODEL_FROM_CODE = "Use Model from Code"
173
+
174
+
175
+ ListOfParameters = Annotated[
176
+ list[Parameter], Field(title=BaseModelTypeTitle.LIST_OF_PARAMETERS.value)
177
+ ]
178
+ JSONSchemaString = Annotated[
179
+ str, Field(title=BaseModelTypeTitle.JSON_SCHEMA_AS_STRING.value)
180
+ ]
181
+ CodefinedModelType = Annotated[
182
+ None, Field(title=BaseModelTypeTitle.USE_MODEL_FROM_CODE.value)
183
+ ]
184
+
185
+
186
+ BaseModelType = Annotated[
187
+ type[TModel],
188
+ BeforeValidator(
189
+ convert_to_base_model_type,
190
+ json_schema_input_type=ListOfParameters | JSONSchemaString | CodefinedModelType,
191
+ ),
192
+ ]
193
+
194
+
195
+ def get_json_schema_extra_for_base_model_type(model: type[BaseModel]):
196
+ """
197
+ Returns a json_schema_extra mutator that injects defaults
198
+ into both the 'string' and 'list[Parameter]' branches.
199
+
200
+ This is used to define default for the "oneOf"/"anyOf" validation
201
+ of the parameters attribute.
202
+ """
203
+ sample_params = base_model_to_parameter_list(model)
204
+
205
+ def _mutate(schema: dict) -> None:
206
+ json_default = json.dumps(model.model_json_schema())
207
+ params_default = [p.model_dump() for p in sample_params]
208
+
209
+ for key in ("oneOf", "anyOf"):
210
+ if key in schema:
211
+ for entry in schema[key]:
212
+ if (
213
+ entry.get("type") == "string"
214
+ and entry.get("title")
215
+ == BaseModelTypeTitle.JSON_SCHEMA_AS_STRING.value
216
+ ):
217
+ entry["default"] = json_default
218
+ if (
219
+ entry.get("type") == "array"
220
+ and entry.get("title")
221
+ == BaseModelTypeTitle.LIST_OF_PARAMETERS.value
222
+ ):
223
+ entry["default"] = params_default
224
+
225
+ return _mutate
226
+
227
+
228
+ if __name__ == "__main__":
229
+ import json
230
+ from pathlib import Path
231
+ from typing import Generic
232
+
233
+ from openai.types.chat.chat_completion_tool_param import ChatCompletionToolParam
234
+ from openai.types.shared_params.function_definition import FunctionDefinition
235
+ from pydantic import BaseModel, Field, field_serializer
236
+
237
+ class ToolDescription(BaseModel, Generic[TModel]):
238
+ name: str = Field(
239
+ ...,
240
+ pattern=r"^[a-zA-Z1-9_-]+$",
241
+ description="Name must adhere to the pattern ^[a-zA-Z1-9_-]+$",
242
+ )
243
+ description: str = Field(
244
+ ...,
245
+ description="Description of what the tool is doing the tool",
246
+ )
247
+
248
+ strict: bool = Field(
249
+ default=False,
250
+ description="Setting strict to true will ensure function calls reliably adhere to the function schema, instead of being best effort.",
251
+ )
252
+
253
+ parameters: BaseModelType[TModel] = Field(
254
+ ...,
255
+ description="Json Schema for the tool parameters. Must be valid JSON Schema and able to convert to a Pydantic model",
256
+ )
257
+
258
+ @field_serializer("parameters")
259
+ def serialize_parameters(self, parameters: type[BaseModel]):
260
+ return parameters.model_json_schema()
261
+
262
+ def to_openai(self) -> ChatCompletionToolParam:
263
+ return ChatCompletionToolParam(
264
+ function=FunctionDefinition(
265
+ name=self.name,
266
+ description=self.description,
267
+ parameters=self.parameters.model_json_schema(),
268
+ strict=self.strict,
269
+ ),
270
+ type="function",
271
+ )
272
+
273
+ class WeatherToolParameterModel(BaseModel):
274
+ lon: float = Field(
275
+ ..., description="The longitude of the location to get the weather for"
276
+ )
277
+ lat: float = Field(
278
+ ..., description="The latitude of the location to get the weather for"
279
+ )
280
+ name: str = Field(
281
+ ..., description="The name of the location to get the weather for"
282
+ )
283
+
284
+ class GetWeatherTool(ToolDescription[WeatherToolParameterModel]):
285
+ parameters: BaseModelType[WeatherToolParameterModel] = Field(
286
+ default=WeatherToolParameterModel,
287
+ json_schema_extra=get_json_schema_extra_for_base_model_type(
288
+ WeatherToolParameterModel
289
+ ),
290
+ )
291
+
292
+ # The json schema can be used in the RSJF library to create a valid frontend component.
293
+ # You can test it on https://rjsf-team.github.io/react-jsonschema-form/
294
+ file = Path(__file__).parent / "weather_tool_schema.json"
295
+ with file.open("w") as f:
296
+ f.write(json.dumps(GetWeatherTool.model_json_schema(), indent=2))
297
+
298
+ # Notice that the t.parameters is a pydantic model with type annotations
299
+ t = GetWeatherTool(
300
+ name="GetWeather", description="Get the weather for a given location"
301
+ )
302
+ t.parameters(lon=100, lat=100, name="Test")
303
+ print(t.model_dump())