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,374 @@
1
+ import logging
2
+ import time
3
+ from collections import Counter
4
+ from typing import Any, overload
5
+
6
+ from typing_extensions import deprecated
7
+
8
+ from unique_toolkit._common.chunk_relevancy_sorter.config import (
9
+ ChunkRelevancySortConfig,
10
+ )
11
+ from unique_toolkit._common.chunk_relevancy_sorter.exception import (
12
+ ChunkRelevancySorterException,
13
+ )
14
+ from unique_toolkit._common.chunk_relevancy_sorter.schemas import (
15
+ ChunkRelevancy,
16
+ ChunkRelevancySorterResult,
17
+ )
18
+ from unique_toolkit._common.validate_required_values import validate_required_values
19
+ from unique_toolkit.agentic.evaluation.config import EvaluationMetricConfig
20
+ from unique_toolkit.agentic.evaluation.context_relevancy.schema import (
21
+ EvaluationSchemaStructuredOutput,
22
+ StructuredOutputConfig,
23
+ )
24
+ from unique_toolkit.agentic.evaluation.context_relevancy.service import (
25
+ ContextRelevancyEvaluator,
26
+ )
27
+ from unique_toolkit.agentic.evaluation.exception import EvaluatorException
28
+ from unique_toolkit.agentic.evaluation.schemas import (
29
+ EvaluationMetricInput,
30
+ EvaluationMetricName,
31
+ EvaluationMetricResult,
32
+ )
33
+ from unique_toolkit.app.performance.async_tasks import run_async_tasks_parallel
34
+ from unique_toolkit.app.schemas import BaseEvent, ChatEvent
35
+ from unique_toolkit.content.schemas import ContentChunk
36
+ from unique_toolkit.language_model.infos import LanguageModelInfo
37
+
38
+
39
+ class ChunkRelevancySorter:
40
+ @deprecated(
41
+ "Use __init__ with company_id and user_id instead or use the classmethod `from_event`"
42
+ )
43
+ @overload
44
+ def __init__(self, event: ChatEvent | BaseEvent):
45
+ """
46
+ Initialize the ChunkRelevancySorter with an event (deprecated)
47
+ """
48
+
49
+ @overload
50
+ def __init__(self, *, company_id: str, user_id: str):
51
+ """
52
+ Initialize the ChunkRelevancySorter with a company_id and user_id
53
+ """
54
+
55
+ def __init__(
56
+ self,
57
+ event: ChatEvent | BaseEvent | None = None,
58
+ company_id: str | None = None,
59
+ user_id: str | None = None,
60
+ ):
61
+ if isinstance(event, (ChatEvent, BaseEvent)):
62
+ self.chunk_relevancy_evaluator = ContextRelevancyEvaluator.from_event(event)
63
+ else:
64
+ [company_id, user_id] = validate_required_values([company_id, user_id])
65
+ self.chunk_relevancy_evaluator = ContextRelevancyEvaluator(
66
+ company_id=company_id, user_id=user_id
67
+ )
68
+ module_name = "ChunkRelevancySorter"
69
+ self.logger = logging.getLogger(f"{module_name}.{__name__}")
70
+
71
+ @classmethod
72
+ def from_event(cls, event: ChatEvent | BaseEvent):
73
+ return cls(company_id=event.company_id, user_id=event.user_id)
74
+
75
+ async def run(
76
+ self,
77
+ input_text: str,
78
+ chunks: list[ContentChunk],
79
+ config: ChunkRelevancySortConfig,
80
+ ) -> ChunkRelevancySorterResult:
81
+ """
82
+ Resorts the search chunks by classifying each chunk into High, Medium, Low depending on the relevancy to the user input, then
83
+ sorts the chunks based on the classification while preserving the orginial order.
84
+
85
+ Args:
86
+ chunks (list[ContentChunk]): The list of search chunks to be reranked.
87
+
88
+ Returns:
89
+ ChunkRelevancySorterResult: The result of the chunk relevancy sort.
90
+
91
+ Raises:
92
+ ChunkRelevancySorterException: If an error occurs while sorting the chunks.
93
+ """
94
+
95
+ if not config.enabled:
96
+ self.logger.info("Chunk relevancy sort is disabled.")
97
+ return ChunkRelevancySorterResult.from_chunks(chunks)
98
+
99
+ self.logger.info("Running chunk relevancy sort.")
100
+ return await self._run_chunk_relevancy_sort(input_text, chunks, config)
101
+
102
+ async def _run_chunk_relevancy_sort(
103
+ self,
104
+ input_text: str,
105
+ chunks: list[ContentChunk],
106
+ config: ChunkRelevancySortConfig,
107
+ ) -> ChunkRelevancySorterResult:
108
+ start_time = time.time()
109
+
110
+ resorted_relevancies = []
111
+
112
+ try:
113
+ self.logger.info(f"Resorting {len(chunks)} chunks based on relevancy...")
114
+ chunk_relevancies = await self._evaluate_chunks_relevancy(
115
+ input_text,
116
+ chunks,
117
+ config,
118
+ )
119
+ resorted_relevancies = await self._validate_and_sort_relevant_chunks(
120
+ config,
121
+ chunk_relevancies,
122
+ )
123
+ except ChunkRelevancySorterException as e:
124
+ self.logger.error(e.error_message)
125
+ raise e
126
+ except Exception as e:
127
+ unknown_error_msg = "Unknown error occurred while resorting search results."
128
+ raise ChunkRelevancySorterException(
129
+ user_message=f"{unknown_error_msg}. Fallback to original search results.",
130
+ error_message=f"{unknown_error_msg}: {e}",
131
+ )
132
+ finally:
133
+ end_time = time.time()
134
+ duration = end_time - start_time
135
+ total_chunks = len(resorted_relevancies)
136
+ success_msg = f"Resorted {total_chunks} chunks in {duration:.2f} seconds."
137
+ self.logger.info(success_msg)
138
+ return ChunkRelevancySorterResult(
139
+ relevancies=resorted_relevancies,
140
+ user_message=success_msg,
141
+ )
142
+
143
+ async def _evaluate_chunks_relevancy(
144
+ self,
145
+ input_text: str,
146
+ chunks: list[ContentChunk],
147
+ config: ChunkRelevancySortConfig,
148
+ ) -> list[ChunkRelevancy]:
149
+ """
150
+ Evaluates the relevancy of the chunks.
151
+ """
152
+ self.logger.info(
153
+ f"Processing chunk relevancy for {len(chunks)} chunks with {config.language_model.name}. "
154
+ f"(Structured output: {config.structured_output_config.enabled}. Extract fact list: {config.structured_output_config.extract_fact_list})",
155
+ )
156
+
157
+ # Evaluate the relevancy of each chunk
158
+ tasks = [
159
+ self._process_relevancy_evaluation(input_text, chunk=chunk, config=config)
160
+ for chunk in chunks
161
+ ]
162
+ chunk_relevancies = await run_async_tasks_parallel(
163
+ tasks=tasks,
164
+ max_tasks=config.max_tasks,
165
+ logger=self.logger,
166
+ )
167
+
168
+ # handle exceptions
169
+ for chunk_relevancy in chunk_relevancies:
170
+ if isinstance(chunk_relevancy, Exception):
171
+ error_msg = "Error occurred while evaluating context relevancy of a specific chunk"
172
+ raise ChunkRelevancySorterException(
173
+ user_message=f"{error_msg}. Fallback to original search results.",
174
+ error_message=f"{error_msg}: {chunk_relevancy}",
175
+ exception=chunk_relevancy,
176
+ )
177
+
178
+ # This check is currently necessary for typing purposes only
179
+ # as the run_async_tasks_parallel function does not enforce the return type
180
+ # TODO fix return type in run_async_tasks_parallel
181
+ chunk_relevancies = [
182
+ chunk_relevancy
183
+ for chunk_relevancy in chunk_relevancies
184
+ if isinstance(chunk_relevancy, ChunkRelevancy)
185
+ ]
186
+
187
+ return chunk_relevancies
188
+
189
+ async def _evaluate_chunk_relevancy(
190
+ self,
191
+ input_text: str,
192
+ langugage_model: LanguageModelInfo,
193
+ chunk: ContentChunk,
194
+ structured_output_config: StructuredOutputConfig,
195
+ additional_llm_options: dict[str, Any],
196
+ ) -> EvaluationMetricResult | None:
197
+ """
198
+ Gets the relevancy score of the chunk compared to the user message txt.
199
+ """
200
+ structured_output_schema = (
201
+ (
202
+ EvaluationSchemaStructuredOutput.get_with_descriptions(
203
+ structured_output_config
204
+ )
205
+ )
206
+ if structured_output_config.enabled
207
+ else None
208
+ )
209
+
210
+ metric_config = EvaluationMetricConfig(
211
+ enabled=True,
212
+ name=EvaluationMetricName.CONTEXT_RELEVANCY,
213
+ language_model=langugage_model,
214
+ additional_llm_options=additional_llm_options,
215
+ )
216
+ relevancy_input = EvaluationMetricInput(
217
+ input_text=input_text,
218
+ context_texts=[chunk.text],
219
+ )
220
+
221
+ return await self.chunk_relevancy_evaluator.analyze(
222
+ input=relevancy_input,
223
+ config=metric_config,
224
+ structured_output_schema=structured_output_schema,
225
+ )
226
+
227
+ async def _process_relevancy_evaluation(
228
+ self,
229
+ input_text: str,
230
+ chunk: ContentChunk,
231
+ config: ChunkRelevancySortConfig,
232
+ ):
233
+ model = config.language_model
234
+ fallback_model = config.fallback_language_model
235
+ try:
236
+ relevancy = await self._evaluate_chunk_relevancy(
237
+ input_text=input_text,
238
+ langugage_model=model,
239
+ chunk=chunk,
240
+ structured_output_config=config.structured_output_config,
241
+ additional_llm_options=config.additional_llm_options,
242
+ )
243
+ return ChunkRelevancy(
244
+ chunk=chunk,
245
+ relevancy=relevancy,
246
+ )
247
+ except EvaluatorException as e:
248
+ if e.exception:
249
+ self.logger.warning(
250
+ "Error evaluating chunk ID %s with model %s. Trying fallback model %s.",
251
+ chunk.chunk_id,
252
+ model,
253
+ e.exception,
254
+ )
255
+ relevancy = await self._evaluate_chunk_relevancy(
256
+ input_text=input_text,
257
+ langugage_model=fallback_model,
258
+ chunk=chunk,
259
+ structured_output_config=config.structured_output_config,
260
+ additional_llm_options=config.additional_llm_options,
261
+ )
262
+ else:
263
+ raise e
264
+ except Exception as e:
265
+ raise ChunkRelevancySorterException(
266
+ user_message="Error occurred while evaluating context relevancy of a specific chunk.",
267
+ error_message=f"Error in _process_relevancy_evaluation: {e}",
268
+ exception=e,
269
+ )
270
+
271
+ async def _validate_and_sort_relevant_chunks(
272
+ self,
273
+ config: ChunkRelevancySortConfig,
274
+ chunk_relevancies: list[ChunkRelevancy],
275
+ ) -> list[ChunkRelevancy]:
276
+ """
277
+ Checks for error or no value in chunk relevancy.
278
+ """
279
+
280
+ # Check that all chunk relevancies have a relevancy level
281
+ await self._validate_chunk_relevancies(chunk_relevancies)
282
+
283
+ # Filter the chunks based on the relevancy levels to consider
284
+ chunk_relevancies = await self._filter_chunks_by_relevancy_levels(
285
+ config,
286
+ chunk_relevancies,
287
+ )
288
+
289
+ # Sort the chunks based on the relevancy levels
290
+ sorted_chunks = await self._sort_chunk_relevancies_by_relevancy_and_chunk(
291
+ config,
292
+ chunk_relevancies,
293
+ )
294
+
295
+ return sorted_chunks
296
+
297
+ async def _validate_chunk_relevancies(
298
+ self,
299
+ chunk_relevancies: list[ChunkRelevancy],
300
+ ):
301
+ for chunk_relevancy in chunk_relevancies:
302
+ if not chunk_relevancy.relevancy or not chunk_relevancy.relevancy.value:
303
+ raise ChunkRelevancySorterException(
304
+ user_message="Error occurred while evaluating chunk relevancy.",
305
+ error_message=f"No relevancy level returned for chunk ID {chunk_relevancy.chunk.chunk_id}.",
306
+ )
307
+
308
+ async def _sort_chunk_relevancies_by_relevancy_and_chunk(
309
+ self,
310
+ config: ChunkRelevancySortConfig,
311
+ chunk_relevancies: list[ChunkRelevancy],
312
+ ):
313
+ # Define the custom sorting order for relevancy
314
+ relevancy_level_order = config.relevancy_level_order
315
+
316
+ # Create a dictionary to map the chunk chunkId to its position in the original order
317
+ chunk_order = {
318
+ relevancy.chunk.chunk_id: index
319
+ for index, relevancy in enumerate(chunk_relevancies)
320
+ }
321
+
322
+ # Sort the chunk relevancies first by relevancy and then by original order within each relevancy level
323
+ sorted_chunk_relevancies = sorted(
324
+ chunk_relevancies,
325
+ key=lambda obj: (
326
+ relevancy_level_order[obj.relevancy.value.lower()], # type: ignore
327
+ chunk_order[obj.chunk.chunk_id],
328
+ ),
329
+ )
330
+
331
+ # Count and print the distinct values of relevancy
332
+ self._count_distinct_values(sorted_chunk_relevancies)
333
+
334
+ # Return only the chunk in the sorted order
335
+ return sorted_chunk_relevancies
336
+
337
+ async def _filter_chunks_by_relevancy_levels(
338
+ self,
339
+ config: ChunkRelevancySortConfig,
340
+ chunk_relevancies: list[ChunkRelevancy],
341
+ ) -> list[ChunkRelevancy]:
342
+ levels_to_consider = [
343
+ relevancy_level.lower()
344
+ for relevancy_level in config.relevancy_levels_to_consider
345
+ ]
346
+ if not levels_to_consider:
347
+ self.logger.warning("No relevancy levels defined, including all levels.")
348
+ return chunk_relevancies
349
+
350
+ self.logger.info(
351
+ "Filtering chunks by relevancy levels: %s.", levels_to_consider
352
+ )
353
+ return [
354
+ chunk_relevancy
355
+ for chunk_relevancy in chunk_relevancies
356
+ if chunk_relevancy.relevancy.value.lower() in levels_to_consider # type: ignore
357
+ ]
358
+
359
+ def _count_distinct_values(self, chunk_relevancies: list[ChunkRelevancy]):
360
+ # Extract the values from the relevancy field
361
+ values = [
362
+ cr.relevancy.value
363
+ for cr in chunk_relevancies
364
+ if cr.relevancy and cr.relevancy.value
365
+ ]
366
+
367
+ # Use Counter to count occurrences
368
+ value_counts = Counter(values)
369
+
370
+ self.logger.info("Count of distinct relevancy values:")
371
+ for value, count in value_counts.items():
372
+ self.logger.info(f"Relevancy: {value}, Count: {count}")
373
+
374
+ return value_counts
@@ -0,0 +1,275 @@
1
+ from unittest.mock import MagicMock, patch
2
+
3
+ import pytest
4
+
5
+ from unique_toolkit._common.chunk_relevancy_sorter.config import (
6
+ ChunkRelevancySortConfig,
7
+ )
8
+ from unique_toolkit._common.chunk_relevancy_sorter.exception import (
9
+ ChunkRelevancySorterException,
10
+ )
11
+ from unique_toolkit._common.chunk_relevancy_sorter.schemas import (
12
+ ChunkRelevancy,
13
+ ChunkRelevancySorterResult,
14
+ )
15
+ from unique_toolkit._common.chunk_relevancy_sorter.service import ChunkRelevancySorter
16
+ from unique_toolkit.agentic.evaluation.context_relevancy.schema import (
17
+ StructuredOutputConfig,
18
+ )
19
+ from unique_toolkit.agentic.evaluation.schemas import (
20
+ EvaluationMetricName,
21
+ EvaluationMetricResult,
22
+ )
23
+ from unique_toolkit.app.schemas import ChatEvent
24
+ from unique_toolkit.content.schemas import ContentChunk
25
+ from unique_toolkit.language_model.default_language_model import DEFAULT_GPT_4o
26
+ from unique_toolkit.language_model.infos import LanguageModelInfo
27
+
28
+
29
+ @pytest.fixture
30
+ def event():
31
+ event = MagicMock(spec=ChatEvent)
32
+ event.payload = MagicMock()
33
+ event.payload.user_message = MagicMock()
34
+ event.payload.user_message.text = "Test query"
35
+ event.user_id = "user_0"
36
+ event.company_id = "company_0"
37
+ return event
38
+
39
+
40
+ @pytest.fixture
41
+ def mock_chunks():
42
+ return [
43
+ ContentChunk(
44
+ id=f"chunk_{i}",
45
+ order=i,
46
+ chunk_id=f"chunk_{i}",
47
+ text=f"Test content {i}",
48
+ )
49
+ for i in range(3)
50
+ ]
51
+
52
+
53
+ @pytest.fixture
54
+ def config():
55
+ return ChunkRelevancySortConfig(
56
+ enabled=True,
57
+ relevancy_levels_to_consider=["high", "medium", "low"],
58
+ relevancy_level_order={"high": 0, "medium": 1, "low": 2},
59
+ language_model=LanguageModelInfo.from_name(DEFAULT_GPT_4o),
60
+ fallback_language_model=LanguageModelInfo.from_name(DEFAULT_GPT_4o),
61
+ structured_output_config=StructuredOutputConfig(
62
+ enabled=False,
63
+ extract_fact_list=False,
64
+ ),
65
+ )
66
+
67
+
68
+ @pytest.fixture
69
+ def chunk_relevancy_sorter(event):
70
+ return ChunkRelevancySorter(event)
71
+
72
+
73
+ @pytest.mark.asyncio
74
+ async def test_run_disabled_config(chunk_relevancy_sorter, mock_chunks, config):
75
+ config.enabled = False
76
+ result = await chunk_relevancy_sorter.run("test input", mock_chunks, config)
77
+
78
+ assert isinstance(result, ChunkRelevancySorterResult)
79
+ assert result.content_chunks == mock_chunks
80
+ assert len(result.content_chunks) == len(mock_chunks)
81
+
82
+
83
+ @pytest.mark.asyncio
84
+ async def test_run_enabled_config(chunk_relevancy_sorter, mock_chunks, config):
85
+ with patch.object(chunk_relevancy_sorter, "_run_chunk_relevancy_sort") as mock_sort:
86
+ mock_sort.return_value = ChunkRelevancySorterResult.from_chunks(mock_chunks)
87
+
88
+ result = await chunk_relevancy_sorter.run("test input", mock_chunks, config)
89
+
90
+ assert isinstance(result, ChunkRelevancySorterResult)
91
+ assert result.content_chunks == mock_chunks
92
+ mock_sort.assert_called_once_with("test input", mock_chunks, config)
93
+
94
+
95
+ @pytest.mark.asyncio
96
+ async def test_evaluate_chunks_relevancy(chunk_relevancy_sorter, mock_chunks, config):
97
+ mock_relevancy = EvaluationMetricResult(
98
+ value="high",
99
+ name=EvaluationMetricName.CONTEXT_RELEVANCY,
100
+ reason="Test reason",
101
+ )
102
+
103
+ with patch.object(
104
+ chunk_relevancy_sorter, "_process_relevancy_evaluation"
105
+ ) as mock_process:
106
+ mock_process.return_value = ChunkRelevancy(
107
+ chunk=mock_chunks[0], relevancy=mock_relevancy
108
+ )
109
+
110
+ result = await chunk_relevancy_sorter._evaluate_chunks_relevancy(
111
+ "test input", mock_chunks, config
112
+ )
113
+
114
+ assert len(result) == len(mock_chunks)
115
+ assert all(isinstance(r, ChunkRelevancy) for r in result)
116
+ assert mock_process.call_count == len(mock_chunks)
117
+
118
+
119
+ @pytest.mark.asyncio
120
+ async def test_evaluate_chunk_relevancy(chunk_relevancy_sorter, mock_chunks, config):
121
+ with patch(
122
+ "unique_toolkit._common.chunk_relevancy_sorter.service.ContextRelevancyEvaluator.analyze"
123
+ ) as mock_analyze:
124
+ mock_analyze.return_value = EvaluationMetricResult(
125
+ value="high",
126
+ name=EvaluationMetricName.CONTEXT_RELEVANCY,
127
+ reason="Test reason",
128
+ )
129
+
130
+ result = await chunk_relevancy_sorter._evaluate_chunk_relevancy(
131
+ input_text="test input",
132
+ chunk=mock_chunks[0],
133
+ langugage_model=config.language_model,
134
+ structured_output_config=config.structured_output_config,
135
+ additional_llm_options=config.additional_llm_options,
136
+ )
137
+
138
+ assert isinstance(result, EvaluationMetricResult)
139
+ assert result.value == "high"
140
+ mock_analyze.assert_called_once()
141
+
142
+
143
+ @pytest.mark.asyncio
144
+ async def test_process_relevancy_evaluation_success(
145
+ chunk_relevancy_sorter, mock_chunks, config
146
+ ):
147
+ with patch.object(
148
+ chunk_relevancy_sorter, "_evaluate_chunk_relevancy"
149
+ ) as mock_evaluate:
150
+ mock_evaluate.return_value = EvaluationMetricResult(
151
+ value="high",
152
+ name=EvaluationMetricName.CONTEXT_RELEVANCY,
153
+ reason="Test reason",
154
+ )
155
+
156
+ result = await chunk_relevancy_sorter._process_relevancy_evaluation(
157
+ "test input", mock_chunks[0], config
158
+ )
159
+
160
+ assert isinstance(result, ChunkRelevancy)
161
+ assert result.chunk == mock_chunks[0]
162
+ assert result.relevancy is not None
163
+ assert result.relevancy.value == "high"
164
+
165
+
166
+ @pytest.mark.asyncio
167
+ async def test_process_relevancy_evaluation_fallback(
168
+ chunk_relevancy_sorter, mock_chunks, config
169
+ ):
170
+ with patch.object(
171
+ chunk_relevancy_sorter, "_evaluate_chunk_relevancy"
172
+ ) as mock_evaluate:
173
+ # First call raises exception, second call succeeds
174
+ mock_evaluate.side_effect = [
175
+ Exception("Test error"),
176
+ EvaluationMetricResult(
177
+ value="medium",
178
+ name=EvaluationMetricName.CONTEXT_RELEVANCY,
179
+ reason="Test reason",
180
+ ),
181
+ ]
182
+
183
+ with pytest.raises(ChunkRelevancySorterException):
184
+ await chunk_relevancy_sorter._process_relevancy_evaluation(
185
+ "test input", mock_chunks[0], config
186
+ )
187
+
188
+
189
+ @pytest.mark.asyncio
190
+ async def test_validate_and_sort_relevant_chunks(
191
+ chunk_relevancy_sorter, mock_chunks, config
192
+ ):
193
+ chunk_relevancies = [
194
+ ChunkRelevancy(
195
+ chunk=mock_chunks[0],
196
+ relevancy=EvaluationMetricResult(
197
+ value="low",
198
+ name=EvaluationMetricName.CONTEXT_RELEVANCY,
199
+ reason="Test reason",
200
+ ),
201
+ )
202
+ ]
203
+ chunk_relevancies.append(
204
+ ChunkRelevancy(
205
+ chunk=mock_chunks[1],
206
+ relevancy=EvaluationMetricResult(
207
+ value="medium",
208
+ name=EvaluationMetricName.CONTEXT_RELEVANCY,
209
+ reason="Test reason",
210
+ ),
211
+ )
212
+ )
213
+ chunk_relevancies.append(
214
+ ChunkRelevancy(
215
+ chunk=mock_chunks[2],
216
+ relevancy=EvaluationMetricResult(
217
+ value="high",
218
+ name=EvaluationMetricName.CONTEXT_RELEVANCY,
219
+ reason="Test reason",
220
+ ),
221
+ )
222
+ )
223
+
224
+ result = await chunk_relevancy_sorter._validate_and_sort_relevant_chunks(
225
+ config, chunk_relevancies
226
+ )
227
+
228
+ assert isinstance(result, list)
229
+ assert len(result) == len(mock_chunks)
230
+ assert all(isinstance(relevancy.chunk, ContentChunk) for relevancy in result)
231
+ assert result[0].chunk == mock_chunks[2]
232
+ assert result[1].chunk == mock_chunks[1]
233
+ assert result[2].chunk == mock_chunks[0]
234
+
235
+
236
+ @pytest.mark.asyncio
237
+ async def test_validate_chunk_relevancies_invalid(chunk_relevancy_sorter):
238
+ invalid_relevancies = [
239
+ ChunkRelevancy(
240
+ chunk=ContentChunk(chunk_id="test", text="test", id="test", order=0),
241
+ relevancy=None,
242
+ )
243
+ ]
244
+
245
+ with pytest.raises(ChunkRelevancySorterException):
246
+ await chunk_relevancy_sorter._validate_chunk_relevancies(invalid_relevancies)
247
+
248
+
249
+ def test_count_distinct_values(chunk_relevancy_sorter, mock_chunks):
250
+ chunk_relevancies = [
251
+ ChunkRelevancy(
252
+ chunk=chunk,
253
+ relevancy=EvaluationMetricResult(
254
+ value="high",
255
+ name=EvaluationMetricName.CONTEXT_RELEVANCY,
256
+ reason="Test reason",
257
+ ),
258
+ )
259
+ for chunk in mock_chunks[:2]
260
+ ]
261
+ chunk_relevancies.append(
262
+ ChunkRelevancy(
263
+ chunk=mock_chunks[2],
264
+ relevancy=EvaluationMetricResult(
265
+ value="medium",
266
+ name=EvaluationMetricName.CONTEXT_RELEVANCY,
267
+ reason="Test reason",
268
+ ),
269
+ )
270
+ )
271
+
272
+ value_counts = chunk_relevancy_sorter._count_distinct_values(chunk_relevancies)
273
+
274
+ assert value_counts["high"] == 2
275
+ assert value_counts["medium"] == 1
@@ -0,0 +1,12 @@
1
+ import warnings
2
+
3
+ from unique_toolkit.language_model.infos import LanguageModelName
4
+
5
+ warnings.warn(
6
+ "unique_toolkit._common.default_language_model is deprecated. "
7
+ "Import DEFAULT_GPT_4o from unique_toolkit.language_model instead.",
8
+ DeprecationWarning,
9
+ stacklevel=2,
10
+ )
11
+
12
+ DEFAULT_GPT_4o = LanguageModelName.AZURE_GPT_4o_2024_1120
@@ -0,0 +1,7 @@
1
+ from unique_toolkit._common.docx_generator.config import DocxGeneratorConfig
2
+ from unique_toolkit._common.docx_generator.service import DocxGeneratorService
3
+
4
+ __all__ = [
5
+ "DocxGeneratorService",
6
+ "DocxGeneratorConfig",
7
+ ]
@@ -0,0 +1,12 @@
1
+ from pydantic import BaseModel, Field
2
+
3
+ from unique_toolkit._common.pydantic_helpers import get_configuration_dict
4
+
5
+
6
+ class DocxGeneratorConfig(BaseModel):
7
+ model_config = get_configuration_dict()
8
+
9
+ template_content_id: str = Field(
10
+ default="",
11
+ description="The content id of the template file uploaded to the knowledge base.",
12
+ )