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.
- unique_toolkit/__init__.py +28 -1
- unique_toolkit/_common/api_calling/human_verification_manager.py +343 -0
- unique_toolkit/_common/base_model_type_attribute.py +303 -0
- unique_toolkit/_common/chunk_relevancy_sorter/config.py +49 -0
- unique_toolkit/_common/chunk_relevancy_sorter/exception.py +5 -0
- unique_toolkit/_common/chunk_relevancy_sorter/schemas.py +46 -0
- unique_toolkit/_common/chunk_relevancy_sorter/service.py +374 -0
- unique_toolkit/_common/chunk_relevancy_sorter/tests/test_service.py +275 -0
- unique_toolkit/_common/default_language_model.py +12 -0
- unique_toolkit/_common/docx_generator/__init__.py +7 -0
- unique_toolkit/_common/docx_generator/config.py +12 -0
- unique_toolkit/_common/docx_generator/schemas.py +80 -0
- unique_toolkit/_common/docx_generator/service.py +252 -0
- unique_toolkit/_common/docx_generator/template/Doc Template.docx +0 -0
- unique_toolkit/_common/endpoint_builder.py +305 -0
- unique_toolkit/_common/endpoint_requestor.py +430 -0
- unique_toolkit/_common/exception.py +24 -0
- unique_toolkit/_common/feature_flags/schema.py +9 -0
- unique_toolkit/_common/pydantic/rjsf_tags.py +936 -0
- unique_toolkit/_common/pydantic_helpers.py +154 -0
- unique_toolkit/_common/referencing.py +53 -0
- unique_toolkit/_common/string_utilities.py +140 -0
- unique_toolkit/_common/tests/test_referencing.py +521 -0
- unique_toolkit/_common/tests/test_string_utilities.py +506 -0
- unique_toolkit/_common/token/image_token_counting.py +67 -0
- unique_toolkit/_common/token/token_counting.py +204 -0
- unique_toolkit/_common/utils/__init__.py +1 -0
- unique_toolkit/_common/utils/files.py +43 -0
- unique_toolkit/_common/utils/structured_output/__init__.py +1 -0
- unique_toolkit/_common/utils/structured_output/schema.py +5 -0
- unique_toolkit/_common/utils/write_configuration.py +51 -0
- unique_toolkit/_common/validators.py +101 -4
- unique_toolkit/agentic/__init__.py +1 -0
- unique_toolkit/agentic/debug_info_manager/debug_info_manager.py +28 -0
- unique_toolkit/agentic/debug_info_manager/test/test_debug_info_manager.py +278 -0
- unique_toolkit/agentic/evaluation/config.py +36 -0
- unique_toolkit/{evaluators → agentic/evaluation}/context_relevancy/prompts.py +25 -0
- unique_toolkit/agentic/evaluation/context_relevancy/schema.py +80 -0
- unique_toolkit/agentic/evaluation/context_relevancy/service.py +273 -0
- unique_toolkit/agentic/evaluation/evaluation_manager.py +218 -0
- unique_toolkit/agentic/evaluation/hallucination/constants.py +61 -0
- unique_toolkit/agentic/evaluation/hallucination/hallucination_evaluation.py +111 -0
- unique_toolkit/{evaluators → agentic/evaluation}/hallucination/prompts.py +1 -1
- unique_toolkit/{evaluators → agentic/evaluation}/hallucination/service.py +16 -15
- unique_toolkit/{evaluators → agentic/evaluation}/hallucination/utils.py +30 -20
- unique_toolkit/{evaluators → agentic/evaluation}/output_parser.py +20 -2
- unique_toolkit/{evaluators → agentic/evaluation}/schemas.py +27 -7
- unique_toolkit/agentic/evaluation/tests/test_context_relevancy_service.py +253 -0
- unique_toolkit/agentic/evaluation/tests/test_output_parser.py +87 -0
- unique_toolkit/agentic/history_manager/history_construction_with_contents.py +297 -0
- unique_toolkit/agentic/history_manager/history_manager.py +242 -0
- unique_toolkit/agentic/history_manager/loop_token_reducer.py +484 -0
- unique_toolkit/agentic/history_manager/utils.py +96 -0
- unique_toolkit/agentic/postprocessor/postprocessor_manager.py +212 -0
- unique_toolkit/agentic/reference_manager/reference_manager.py +103 -0
- unique_toolkit/agentic/responses_api/__init__.py +19 -0
- unique_toolkit/agentic/responses_api/postprocessors/code_display.py +63 -0
- unique_toolkit/agentic/responses_api/postprocessors/generated_files.py +145 -0
- unique_toolkit/agentic/responses_api/stream_handler.py +15 -0
- unique_toolkit/agentic/short_term_memory_manager/persistent_short_term_memory_manager.py +141 -0
- unique_toolkit/agentic/thinking_manager/thinking_manager.py +103 -0
- unique_toolkit/agentic/tools/__init__.py +1 -0
- unique_toolkit/agentic/tools/a2a/__init__.py +36 -0
- unique_toolkit/agentic/tools/a2a/config.py +17 -0
- unique_toolkit/agentic/tools/a2a/evaluation/__init__.py +15 -0
- unique_toolkit/agentic/tools/a2a/evaluation/_utils.py +66 -0
- unique_toolkit/agentic/tools/a2a/evaluation/config.py +55 -0
- unique_toolkit/agentic/tools/a2a/evaluation/evaluator.py +260 -0
- unique_toolkit/agentic/tools/a2a/evaluation/summarization_user_message.j2 +9 -0
- unique_toolkit/agentic/tools/a2a/manager.py +55 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/__init__.py +21 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/_display_utils.py +185 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/_ref_utils.py +73 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/config.py +45 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/display.py +180 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/references.py +101 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display_utils.py +1335 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/test/test_ref_utils.py +603 -0
- unique_toolkit/agentic/tools/a2a/prompts.py +46 -0
- unique_toolkit/agentic/tools/a2a/response_watcher/__init__.py +6 -0
- unique_toolkit/agentic/tools/a2a/response_watcher/service.py +91 -0
- unique_toolkit/agentic/tools/a2a/tool/__init__.py +4 -0
- unique_toolkit/agentic/tools/a2a/tool/_memory.py +26 -0
- unique_toolkit/agentic/tools/a2a/tool/_schema.py +9 -0
- unique_toolkit/agentic/tools/a2a/tool/config.py +73 -0
- unique_toolkit/agentic/tools/a2a/tool/service.py +306 -0
- unique_toolkit/agentic/tools/agent_chunks_hanlder.py +65 -0
- unique_toolkit/agentic/tools/config.py +167 -0
- unique_toolkit/agentic/tools/factory.py +44 -0
- unique_toolkit/agentic/tools/mcp/__init__.py +4 -0
- unique_toolkit/agentic/tools/mcp/manager.py +71 -0
- unique_toolkit/agentic/tools/mcp/models.py +28 -0
- unique_toolkit/agentic/tools/mcp/tool_wrapper.py +234 -0
- unique_toolkit/agentic/tools/openai_builtin/__init__.py +11 -0
- unique_toolkit/agentic/tools/openai_builtin/base.py +30 -0
- unique_toolkit/agentic/tools/openai_builtin/code_interpreter/__init__.py +8 -0
- unique_toolkit/agentic/tools/openai_builtin/code_interpreter/config.py +57 -0
- unique_toolkit/agentic/tools/openai_builtin/code_interpreter/service.py +230 -0
- unique_toolkit/agentic/tools/openai_builtin/manager.py +62 -0
- unique_toolkit/agentic/tools/schemas.py +141 -0
- unique_toolkit/agentic/tools/test/test_mcp_manager.py +536 -0
- unique_toolkit/agentic/tools/test/test_tool_progress_reporter.py +445 -0
- unique_toolkit/agentic/tools/tool.py +183 -0
- unique_toolkit/agentic/tools/tool_manager.py +523 -0
- unique_toolkit/agentic/tools/tool_progress_reporter.py +285 -0
- unique_toolkit/agentic/tools/utils/__init__.py +19 -0
- unique_toolkit/agentic/tools/utils/execution/__init__.py +1 -0
- unique_toolkit/agentic/tools/utils/execution/execution.py +286 -0
- unique_toolkit/agentic/tools/utils/source_handling/__init__.py +0 -0
- unique_toolkit/agentic/tools/utils/source_handling/schema.py +21 -0
- unique_toolkit/agentic/tools/utils/source_handling/source_formatting.py +207 -0
- unique_toolkit/agentic/tools/utils/source_handling/tests/test_source_formatting.py +216 -0
- unique_toolkit/app/__init__.py +6 -0
- unique_toolkit/app/dev_util.py +180 -0
- unique_toolkit/app/init_sdk.py +32 -1
- unique_toolkit/app/schemas.py +198 -31
- unique_toolkit/app/unique_settings.py +367 -0
- unique_toolkit/chat/__init__.py +8 -1
- unique_toolkit/chat/deprecated/service.py +232 -0
- unique_toolkit/chat/functions.py +642 -77
- unique_toolkit/chat/rendering.py +34 -0
- unique_toolkit/chat/responses_api.py +461 -0
- unique_toolkit/chat/schemas.py +133 -2
- unique_toolkit/chat/service.py +115 -767
- unique_toolkit/content/functions.py +153 -4
- unique_toolkit/content/schemas.py +122 -15
- unique_toolkit/content/service.py +278 -44
- unique_toolkit/content/smart_rules.py +301 -0
- unique_toolkit/content/utils.py +8 -3
- unique_toolkit/embedding/service.py +102 -11
- unique_toolkit/framework_utilities/__init__.py +1 -0
- unique_toolkit/framework_utilities/langchain/client.py +71 -0
- unique_toolkit/framework_utilities/langchain/history.py +19 -0
- unique_toolkit/framework_utilities/openai/__init__.py +6 -0
- unique_toolkit/framework_utilities/openai/client.py +83 -0
- unique_toolkit/framework_utilities/openai/message_builder.py +229 -0
- unique_toolkit/framework_utilities/utils.py +23 -0
- unique_toolkit/language_model/__init__.py +3 -0
- unique_toolkit/language_model/builder.py +27 -11
- unique_toolkit/language_model/default_language_model.py +3 -0
- unique_toolkit/language_model/functions.py +327 -43
- unique_toolkit/language_model/infos.py +992 -50
- unique_toolkit/language_model/reference.py +242 -0
- unique_toolkit/language_model/schemas.py +475 -48
- unique_toolkit/language_model/service.py +228 -27
- unique_toolkit/protocols/support.py +145 -0
- unique_toolkit/services/__init__.py +7 -0
- unique_toolkit/services/chat_service.py +1630 -0
- unique_toolkit/services/knowledge_base.py +861 -0
- unique_toolkit/short_term_memory/service.py +178 -41
- unique_toolkit/smart_rules/__init__.py +0 -0
- unique_toolkit/smart_rules/compile.py +56 -0
- unique_toolkit/test_utilities/events.py +197 -0
- {unique_toolkit-0.7.7.dist-info → unique_toolkit-1.23.0.dist-info}/METADATA +606 -7
- unique_toolkit-1.23.0.dist-info/RECORD +182 -0
- unique_toolkit/evaluators/__init__.py +0 -1
- unique_toolkit/evaluators/config.py +0 -35
- unique_toolkit/evaluators/constants.py +0 -1
- unique_toolkit/evaluators/context_relevancy/constants.py +0 -32
- unique_toolkit/evaluators/context_relevancy/service.py +0 -53
- unique_toolkit/evaluators/context_relevancy/utils.py +0 -142
- unique_toolkit/evaluators/hallucination/constants.py +0 -41
- unique_toolkit-0.7.7.dist-info/RECORD +0 -64
- /unique_toolkit/{evaluators → agentic/evaluation}/exception.py +0 -0
- {unique_toolkit-0.7.7.dist-info → unique_toolkit-1.23.0.dist-info}/LICENSE +0 -0
- {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))
|