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
@@ -0,0 +1,154 @@
1
+ import logging
2
+ import warnings
3
+ from typing import TypeVar, Unpack
4
+
5
+ import humps
6
+ from pydantic import BaseModel, ConfigDict, Field, create_model
7
+ from pydantic.alias_generators import to_camel
8
+ from pydantic.fields import ComputedFieldInfo, FieldInfo
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ def field_title_generator(
14
+ title: str,
15
+ info: FieldInfo | ComputedFieldInfo,
16
+ ) -> str:
17
+ return humps.decamelize(title).replace("_", " ").title()
18
+
19
+
20
+ def model_title_generator(model: type) -> str:
21
+ return humps.decamelize(model.__name__).replace("_", " ").title()
22
+
23
+
24
+ def get_configuration_dict(**kwargs: Unpack[ConfigDict]) -> ConfigDict:
25
+ config = {
26
+ "alias_generator": to_camel,
27
+ "field_title_generator": field_title_generator,
28
+ "model_title_generator": model_title_generator,
29
+ "populate_by_name": True,
30
+ # protected_namespaces=(),
31
+ }
32
+ config.update(kwargs)
33
+ return ConfigDict(**config)
34
+
35
+
36
+ ModelTypeA = TypeVar("ModelTypeA", bound=BaseModel)
37
+ ModelTypeB = TypeVar("ModelTypeB", bound=BaseModel)
38
+
39
+
40
+ def _name_intersection(
41
+ model_type_a: type[ModelTypeA], model_type_b: type[ModelTypeB]
42
+ ) -> set[str]:
43
+ field_names_a = set(model_type_a.model_fields.keys())
44
+ field_names_b = set(model_type_b.model_fields.keys())
45
+ return field_names_a.intersection(field_names_b)
46
+
47
+
48
+ def create_union_model(
49
+ model_type_a: type[ModelTypeA],
50
+ model_type_b: type[ModelTypeB],
51
+ model_name: str = "UnionModel",
52
+ config_dict: ConfigDict = ConfigDict(),
53
+ ) -> type[BaseModel]:
54
+ """
55
+ Creates a model that is the union of the two input models.
56
+ Prefers fields from model_type_a.
57
+ """
58
+
59
+ if len(_name_intersection(model_type_a, model_type_b)) > 0:
60
+ warnings.warn(
61
+ f"The two input models have common field names: {_name_intersection(model_type_a, model_type_b)}"
62
+ )
63
+
64
+ fields = {}
65
+ for name, field in model_type_b.model_fields.items():
66
+ fields[name] = (field.annotation, field)
67
+ for name, field in model_type_a.model_fields.items():
68
+ fields[name] = (field.annotation, field)
69
+
70
+ CombinedModel = create_model(model_name, __config__=config_dict, **fields)
71
+ return CombinedModel
72
+
73
+
74
+ def create_intersection_model(
75
+ model_type_a: type[ModelTypeA],
76
+ model_type_b: type[ModelTypeB],
77
+ model_name: str = "IntersectionModel",
78
+ config_dict: ConfigDict = ConfigDict(),
79
+ ) -> type[BaseModel]:
80
+ """
81
+ Creates a model that is the intersection of the two input models.
82
+ Prefers fields from model_type_a.
83
+ """
84
+
85
+ if len(_name_intersection(model_type_a, model_type_b)) == 0:
86
+ warnings.warn(
87
+ f"The two input models have no common field names: {_name_intersection(model_type_a, model_type_b)}"
88
+ )
89
+
90
+ fields = {}
91
+ field_names1 = set(model_type_a.model_fields.keys())
92
+ field_names2 = set(model_type_b.model_fields.keys())
93
+ common_field_names = field_names1.intersection(field_names2)
94
+
95
+ for name in common_field_names:
96
+ if name in field_names1.intersection(field_names2):
97
+ fields[name] = (
98
+ model_type_a.model_fields[name].annotation,
99
+ model_type_a.model_fields[name],
100
+ )
101
+
102
+ IntersectionModel = create_model(model_name, __config__=config_dict, **fields)
103
+ return IntersectionModel
104
+
105
+
106
+ def create_complement_model(
107
+ model_type_a: type[ModelTypeA],
108
+ model_type_b: type[ModelTypeB],
109
+ model_name: str = "ComplementModel",
110
+ config_dict: ConfigDict = ConfigDict(),
111
+ ) -> type[BaseModel]:
112
+ """
113
+ Creates a model that is the complement of the two input models
114
+ i.e all fields from model_type_a that are not in model_type_b
115
+ """
116
+
117
+ if len(_name_intersection(model_type_a, model_type_b)) == 0:
118
+ warnings.warn(
119
+ f"The two input models have no common field names: {_name_intersection(model_type_a, model_type_b)}"
120
+ )
121
+
122
+ fields = {}
123
+ field_names_a = set(model_type_a.model_fields.keys())
124
+ field_names_b = set(model_type_b.model_fields.keys())
125
+ complement_field_names = field_names_a.difference(field_names_b)
126
+
127
+ for name in complement_field_names:
128
+ fields[name] = (
129
+ model_type_a.model_fields[name].annotation,
130
+ model_type_a.model_fields[name],
131
+ )
132
+
133
+ ComplementModel = create_model(model_name, __config__=config_dict, **fields)
134
+
135
+ return ComplementModel
136
+
137
+
138
+ if __name__ == "__main__":
139
+
140
+ class ModelType1(BaseModel):
141
+ field1: int = Field(default=1, description="Field 1")
142
+ field2: str = Field(
143
+ default="test",
144
+ description="Field 2",
145
+ json_schema_extra={"title": "Field 2"},
146
+ )
147
+
148
+ class ModelType2(BaseModel):
149
+ field3: float
150
+ field4: bool
151
+
152
+ combined_model = create_union_model(ModelType1, ModelType2)
153
+
154
+ print(combined_model.model_fields)
@@ -0,0 +1,53 @@
1
+ """
2
+ Utilities for handling references in the "postprocessed" format, i.e <sup>X</sup>
3
+ """
4
+
5
+ import functools
6
+ import re
7
+ from typing import Generator
8
+
9
+ _REF_DETECTION_PATTERN = re.compile(r"<sup>\s*(?P<reference_number>\d+)\s*</sup>")
10
+
11
+
12
+ def _iter_ref_numbers(text: str) -> Generator[int, None, None]:
13
+ for match in _REF_DETECTION_PATTERN.finditer(text):
14
+ yield int(match.group("reference_number"))
15
+
16
+
17
+ @functools.cache
18
+ def _get_detection_pattern_for_ref(ref_number: int) -> re.Pattern[str]:
19
+ return re.compile(rf"<sup>\s*{ref_number}\s*</sup>")
20
+
21
+
22
+ def get_reference_pattern(ref_number: int) -> str:
23
+ return f"<sup>{ref_number}</sup>"
24
+
25
+
26
+ def get_all_ref_numbers(text: str) -> list[int]:
27
+ return sorted(set(_iter_ref_numbers(text)))
28
+
29
+
30
+ def get_max_ref_number(text: str) -> int | None:
31
+ return max(_iter_ref_numbers(text), default=None)
32
+
33
+
34
+ def replace_ref_number(text: str, ref_number: int, replacement: int | str) -> str:
35
+ if isinstance(replacement, int):
36
+ replacement = get_reference_pattern(replacement)
37
+
38
+ return _get_detection_pattern_for_ref(ref_number).sub(replacement, text)
39
+
40
+
41
+ def remove_ref_number(text: str, ref_number: int) -> str:
42
+ return _get_detection_pattern_for_ref(ref_number).sub("", text)
43
+
44
+
45
+ def remove_all_refs(text: str) -> str:
46
+ return _REF_DETECTION_PATTERN.sub("", text)
47
+
48
+
49
+ def remove_consecutive_ref_space(text: str) -> str:
50
+ """
51
+ Remove spaces between consecutive references.
52
+ """
53
+ return re.sub(r"</sup>\s*<sup>", "</sup><sup>", text)
@@ -0,0 +1,140 @@
1
+ import json
2
+ import re
3
+ from typing import Any, Iterable, Sequence
4
+ from uuid import uuid4
5
+
6
+
7
+ def _is_elementary_type(value: Any) -> bool:
8
+ """Check if a value is an elementary type (str, int, float, bool, None)."""
9
+ return isinstance(value, (str, int, float, bool, type(None)))
10
+
11
+
12
+ def _is_elementary_dict(data: dict[str, Any]) -> bool:
13
+ """Check if all values in the dictionary are elementary types."""
14
+ return all(_is_elementary_type(value) for value in data.values())
15
+
16
+
17
+ def dict_to_markdown_table(data: dict[str, Any]) -> str:
18
+ """
19
+ Convert a dictionary to a markdown table if all values are elementary types,
20
+ otherwise return stringified JSON.
21
+
22
+ Args:
23
+ data: Dictionary to convert
24
+
25
+ Returns:
26
+ Markdown table string or JSON string
27
+ """
28
+ if not isinstance(data, dict):
29
+ return json.dumps(data, indent=2)
30
+
31
+ if not _is_elementary_dict(data):
32
+ return json.dumps(data, indent=2)
33
+
34
+ if not data: # Empty dict
35
+ return "| Key | Value |\n|-----|-------|\n| (empty) | (empty) |"
36
+
37
+ # Create markdown table
38
+ table_lines = ["| Key | Value |", "|-----|-------|"]
39
+
40
+ for key, value in data.items():
41
+ # Handle None values
42
+ if value is None:
43
+ value_str = "null"
44
+ # Handle boolean values
45
+ elif isinstance(value, bool):
46
+ value_str = "true" if value else "false"
47
+ # Handle other values
48
+ else:
49
+ value_str = str(value)
50
+
51
+ # Escape pipe characters in the content
52
+ key_escaped = str(key).replace("|", "\\|")
53
+ value_escaped = value_str.replace("|", "\\|")
54
+
55
+ table_lines.append(f"| {key_escaped} | {value_escaped} |")
56
+
57
+ return "\n".join(table_lines) + "\n"
58
+
59
+
60
+ def extract_dicts_from_string(text: str) -> list[dict[str, Any]]:
61
+ """
62
+ Extract and parse a JSON dictionary from a string.
63
+
64
+ The string should be wrapped in ```json tags. Example:
65
+
66
+ ```json
67
+ {"key": "value"}
68
+ ```
69
+
70
+ Args:
71
+ text: String that may contain JSON
72
+
73
+ Returns:
74
+ Parsed dictionary or None if no valid JSON found
75
+ """
76
+ # Find JSON-like content between ```json and ``` tags
77
+ pattern = r"```json\s*(\{.*?\})\s*```"
78
+ matches = re.findall(pattern, text, re.DOTALL)
79
+
80
+ dictionaries = []
81
+ for match in matches:
82
+ try:
83
+ # Try to parse as JSON
84
+ parsed = json.loads(match)
85
+ if isinstance(parsed, dict):
86
+ dictionaries.append(parsed)
87
+ except json.JSONDecodeError:
88
+ continue
89
+
90
+ return dictionaries
91
+
92
+
93
+ def _replace_in_text_non_overlapping(
94
+ text: str, repls: Iterable[tuple[str | re.Pattern[str], str]]
95
+ ) -> str:
96
+ for pattern, replacement in repls:
97
+ text = re.sub(pattern, replacement, text)
98
+ return text
99
+
100
+
101
+ def replace_in_text(
102
+ text: str, repls: Sequence[tuple[str | re.Pattern[str], str]]
103
+ ) -> str:
104
+ """
105
+ Replace multiple patterns in text without replacement interference.
106
+
107
+ This function performs all replacements independently, preventing cases where
108
+ a replacement value matches another pattern, which would cause unintended
109
+ cascading replacements.
110
+
111
+ Why this is needed:
112
+ - Naive sequential replacements can interfere with each other
113
+ - Example: replacing "foo" -> "bar" and "bar" -> "baz" would incorrectly
114
+ turn "foo" into "baz" if done sequentially
115
+ - This function uses a two-phase approach with UUID placeholders to ensure
116
+ each pattern is replaced exactly once with its intended value
117
+
118
+ Args:
119
+ text: The input text to perform replacements on
120
+ repls: Sequence of (pattern, replacement) tuples where pattern can be
121
+ a string or compiled regex pattern
122
+
123
+ Returns:
124
+ Text with all patterns replaced by their corresponding replacements
125
+
126
+ Example:
127
+ >>> text = "foo and bar"
128
+ >>> repls = [("foo", "bar"), ("bar", "baz")]
129
+ >>> replace_in_text(text, repls)
130
+ "bar and baz" # Both replacements applied independently
131
+ """
132
+ if len(repls) == 0:
133
+ return text
134
+
135
+ placeholders = [uuid4().hex for _ in range(len(repls))]
136
+ orig, repls = zip(*repls)
137
+
138
+ # 2 phase replacement, since the map keys and values can overlap
139
+ text = _replace_in_text_non_overlapping(text, zip(orig, placeholders))
140
+ return _replace_in_text_non_overlapping(text, zip(placeholders, repls))