unique_toolkit 1.1.3__tar.gz → 1.1.4__tar.gz

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 (134) hide show
  1. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/CHANGELOG.md +4 -0
  2. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/PKG-INFO +5 -1
  3. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/pyproject.toml +1 -1
  4. unique_toolkit-1.1.4/unique_toolkit/_common/api_calling/human_verification_manager.py +250 -0
  5. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/_common/endpoint_builder.py +90 -27
  6. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/_common/endpoint_requestor.py +44 -26
  7. unique_toolkit-1.1.4/unique_toolkit/_common/pydantic_helpers.py +153 -0
  8. unique_toolkit-1.1.4/unique_toolkit/_common/string_utilities.py +89 -0
  9. unique_toolkit-1.1.3/unique_toolkit/_common/pydantic_helpers.py +0 -29
  10. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/LICENSE +0 -0
  11. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/README.md +0 -0
  12. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/__init__.py +0 -0
  13. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/_common/_base_service.py +0 -0
  14. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/_common/_time_utils.py +0 -0
  15. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/_common/base_model_type_attribute.py +0 -0
  16. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/_common/chunk_relevancy_sorter/config.py +0 -0
  17. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/_common/chunk_relevancy_sorter/exception.py +0 -0
  18. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/_common/chunk_relevancy_sorter/schemas.py +0 -0
  19. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/_common/chunk_relevancy_sorter/service.py +0 -0
  20. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/_common/chunk_relevancy_sorter/tests/test_service.py +0 -0
  21. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/_common/default_language_model.py +0 -0
  22. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/_common/exception.py +0 -0
  23. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/_common/feature_flags/schema.py +0 -0
  24. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/_common/token/image_token_counting.py +0 -0
  25. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/_common/token/token_counting.py +0 -0
  26. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/_common/utils/structured_output/schema.py +0 -0
  27. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/_common/utils/write_configuration.py +0 -0
  28. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/_common/validate_required_values.py +0 -0
  29. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/_common/validators.py +0 -0
  30. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/__init__.py +0 -0
  31. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/debug_info_manager/debug_info_manager.py +0 -0
  32. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/evaluation/config.py +0 -0
  33. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/evaluation/context_relevancy/prompts.py +0 -0
  34. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/evaluation/context_relevancy/schema.py +0 -0
  35. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/evaluation/context_relevancy/service.py +0 -0
  36. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/evaluation/evaluation_manager.py +0 -0
  37. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/evaluation/exception.py +0 -0
  38. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/evaluation/hallucination/constants.py +0 -0
  39. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/evaluation/hallucination/hallucination_evaluation.py +0 -0
  40. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/evaluation/hallucination/prompts.py +0 -0
  41. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/evaluation/hallucination/service.py +0 -0
  42. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/evaluation/hallucination/utils.py +0 -0
  43. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/evaluation/output_parser.py +0 -0
  44. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/evaluation/schemas.py +0 -0
  45. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/evaluation/tests/test_context_relevancy_service.py +0 -0
  46. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/evaluation/tests/test_output_parser.py +0 -0
  47. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/history_manager/history_construction_with_contents.py +0 -0
  48. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/history_manager/history_manager.py +0 -0
  49. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/history_manager/loop_token_reducer.py +0 -0
  50. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/history_manager/utils.py +0 -0
  51. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/postprocessor/postprocessor_manager.py +0 -0
  52. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/reference_manager/reference_manager.py +0 -0
  53. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/short_term_memory_manager/persistent_short_term_memory_manager.py +0 -0
  54. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/thinking_manager/thinking_manager.py +0 -0
  55. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/tools/__init__.py +0 -0
  56. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/tools/a2a/__init__.py +0 -0
  57. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/tools/a2a/config.py +0 -0
  58. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/tools/a2a/manager.py +0 -0
  59. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/tools/a2a/memory.py +0 -0
  60. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/tools/a2a/schema.py +0 -0
  61. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/tools/a2a/service.py +0 -0
  62. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/tools/agent_chunks_hanlder.py +0 -0
  63. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/tools/config.py +0 -0
  64. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/tools/factory.py +0 -0
  65. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/tools/mcp/__init__.py +0 -0
  66. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/tools/mcp/manager.py +0 -0
  67. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/tools/mcp/models.py +0 -0
  68. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/tools/mcp/tool_wrapper.py +0 -0
  69. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/tools/schemas.py +0 -0
  70. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/tools/test/test_mcp_manager.py +0 -0
  71. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/tools/test/test_tool_progress_reporter.py +0 -0
  72. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/tools/tool.py +0 -0
  73. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/tools/tool_manager.py +0 -0
  74. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/tools/tool_progress_reporter.py +0 -0
  75. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/tools/utils/__init__.py +0 -0
  76. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/tools/utils/execution/__init__.py +0 -0
  77. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/tools/utils/execution/execution.py +0 -0
  78. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/tools/utils/source_handling/__init__.py +0 -0
  79. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/tools/utils/source_handling/schema.py +0 -0
  80. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/tools/utils/source_handling/source_formatting.py +0 -0
  81. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/agentic/tools/utils/source_handling/tests/test_source_formatting.py +0 -0
  82. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/app/__init__.py +0 -0
  83. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/app/dev_util.py +0 -0
  84. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/app/init_logging.py +0 -0
  85. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/app/init_sdk.py +0 -0
  86. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/app/performance/async_tasks.py +0 -0
  87. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/app/performance/async_wrapper.py +0 -0
  88. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/app/schemas.py +0 -0
  89. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/app/unique_settings.py +0 -0
  90. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/app/verification.py +0 -0
  91. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/chat/__init__.py +0 -0
  92. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/chat/constants.py +0 -0
  93. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/chat/functions.py +0 -0
  94. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/chat/schemas.py +0 -0
  95. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/chat/service.py +0 -0
  96. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/chat/state.py +0 -0
  97. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/chat/utils.py +0 -0
  98. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/content/__init__.py +0 -0
  99. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/content/constants.py +0 -0
  100. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/content/functions.py +0 -0
  101. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/content/schemas.py +0 -0
  102. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/content/service.py +0 -0
  103. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/content/utils.py +0 -0
  104. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/embedding/__init__.py +0 -0
  105. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/embedding/constants.py +0 -0
  106. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/embedding/functions.py +0 -0
  107. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/embedding/schemas.py +0 -0
  108. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/embedding/service.py +0 -0
  109. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/embedding/utils.py +0 -0
  110. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/framework_utilities/__init__.py +0 -0
  111. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/framework_utilities/langchain/client.py +0 -0
  112. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/framework_utilities/langchain/history.py +0 -0
  113. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/framework_utilities/openai/__init__.py +0 -0
  114. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/framework_utilities/openai/client.py +0 -0
  115. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/framework_utilities/openai/message_builder.py +0 -0
  116. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/framework_utilities/utils.py +0 -0
  117. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/language_model/__init__.py +0 -0
  118. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/language_model/builder.py +0 -0
  119. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/language_model/constants.py +0 -0
  120. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/language_model/functions.py +0 -0
  121. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/language_model/infos.py +0 -0
  122. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/language_model/prompt.py +0 -0
  123. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/language_model/reference.py +0 -0
  124. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/language_model/schemas.py +0 -0
  125. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/language_model/service.py +0 -0
  126. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/language_model/utils.py +0 -0
  127. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/protocols/support.py +0 -0
  128. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/short_term_memory/__init__.py +0 -0
  129. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/short_term_memory/constants.py +0 -0
  130. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/short_term_memory/functions.py +0 -0
  131. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/short_term_memory/schemas.py +0 -0
  132. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/short_term_memory/service.py +0 -0
  133. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/smart_rules/__init__.py +0 -0
  134. {unique_toolkit-1.1.3 → unique_toolkit-1.1.4}/unique_toolkit/smart_rules/compile.py +0 -0
@@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+
9
+ ## [1.1.4] - 2025-09-23
10
+ - First version human verification on api calls
11
+
8
12
  ## [1.1.3] - 2025-09-23
9
13
  - Updated LMI JSON schema input type to include annotated string field with title
10
14
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: unique_toolkit
3
- Version: 1.1.3
3
+ Version: 1.1.4
4
4
  Summary:
5
5
  License: Proprietary
6
6
  Author: Cedric Klinkert
@@ -118,6 +118,10 @@ All notable changes to this project will be documented in this file.
118
118
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
119
119
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
120
120
 
121
+
122
+ ## [1.1.4] - 2025-09-23
123
+ - First version human verification on api calls
124
+
121
125
  ## [1.1.3] - 2025-09-23
122
126
  - Updated LMI JSON schema input type to include annotated string field with title
123
127
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "unique_toolkit"
3
- version = "1.1.3"
3
+ version = "1.1.4"
4
4
  description = ""
5
5
  authors = [
6
6
  "Cedric Klinkert <cedric.klinkert@unique.ch>",
@@ -0,0 +1,250 @@
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
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
+ RequestorType,
19
+ build_requestor,
20
+ )
21
+ from unique_toolkit._common.pydantic_helpers import create_union_model
22
+ from unique_toolkit._common.string_utilities import (
23
+ dict_to_markdown_table,
24
+ extract_dicts_from_string,
25
+ )
26
+ from unique_toolkit.chat.schemas import ChatMessage, ChatMessageRole
27
+
28
+
29
+ class HumanConfirmation(BaseModel):
30
+ payload_hash: str
31
+ time_stamp: datetime
32
+
33
+
34
+ NEXT_USER_MESSAGE_JINJA2_TEMPLATE = jinja2.Template("""I confirm the api call with the following data:
35
+ ```json
36
+ {{ api_call_as_json }}
37
+ ```""")
38
+
39
+
40
+ ASSISTANT_CONFIRMATION_MESSAGE_JINJA2_TEMPLATE = jinja2.Template("""I would like to call the api with the following data:
41
+
42
+ {{ api_call_as_markdown_table }}
43
+
44
+ [{{ button_text }}](https://prompt={{ next_user_message | urlencode }})""")
45
+
46
+
47
+ class HumanVerificationManagerForApiCalling(
48
+ Generic[
49
+ PathParamsSpec,
50
+ PathParamsType,
51
+ PayloadParamSpec,
52
+ PayloadType,
53
+ ResponseType,
54
+ ]
55
+ ):
56
+ """
57
+ Manages human verification for api calling.
58
+
59
+ The idea is that the manager is able to produce the verification message to the user
60
+ and to detect an api call from the user message.
61
+
62
+ If it detects such a verification message in the user message, it will call the api
63
+ and incorporate the response into the user message.
64
+ """
65
+
66
+ def __init__(
67
+ self,
68
+ *,
69
+ logger: Logger,
70
+ operation: type[
71
+ ApiOperationProtocol[
72
+ PathParamsSpec,
73
+ PathParamsType,
74
+ PayloadParamSpec,
75
+ PayloadType,
76
+ ResponseType,
77
+ ]
78
+ ],
79
+ requestor_type: RequestorType = RequestorType.REQUESTS,
80
+ **kwargs: dict[str, Any],
81
+ ):
82
+ self._logger = logger
83
+ self._operation = operation
84
+
85
+ # Create internal models for this manager instance
86
+
87
+ class ConcreteApiCall(BaseModel):
88
+ confirmation: HumanConfirmation
89
+ payload: self._operation.payload_model() # type: ignore
90
+
91
+ self._api_call_model = ConcreteApiCall
92
+
93
+ self._combined_params_model = create_union_model(
94
+ model_type_a=self._operation.path_params_model(),
95
+ model_type_b=self._operation.payload_model(),
96
+ )
97
+ self._requestor_type = requestor_type
98
+ self._requestor = build_requestor(
99
+ requestor_type=requestor_type,
100
+ operation_type=operation,
101
+ combined_model=self._combined_params_model,
102
+ **kwargs,
103
+ )
104
+
105
+ def detect_api_calls_from_user_message(
106
+ self, *, last_assistant_message: ChatMessage, user_message: str
107
+ ) -> PayloadType | None:
108
+ user_message_dicts = extract_dicts_from_string(user_message)
109
+ if len(user_message_dicts) == 0:
110
+ return None
111
+
112
+ user_message_dicts.reverse()
113
+ for user_message_dict in user_message_dicts:
114
+ try:
115
+ # Convert dict to payload model first, then create payload
116
+ api_call = self._api_call_model.model_validate(
117
+ user_message_dict, by_alias=True, by_name=True
118
+ )
119
+ if self._verify_human_verification(
120
+ api_call.confirmation, last_assistant_message
121
+ ):
122
+ return api_call.payload
123
+ except Exception as e:
124
+ self._logger.error(f"Error detecting api calls from user message: {e}")
125
+
126
+ return None
127
+
128
+ def _verify_human_verification(
129
+ self, confirmation: HumanConfirmation, last_assistant_message: ChatMessage
130
+ ) -> bool:
131
+ if (
132
+ last_assistant_message.role != ChatMessageRole.ASSISTANT
133
+ or last_assistant_message.content is None
134
+ ):
135
+ self._logger.error(
136
+ "Last assistant message is not an assistant message or content is empty."
137
+ )
138
+ return False
139
+
140
+ return confirmation.payload_hash in last_assistant_message.content
141
+
142
+ def _create_next_user_message(self, payload: PayloadType) -> str:
143
+ api_call = self._api_call_model(
144
+ payload=payload,
145
+ confirmation=HumanConfirmation(
146
+ payload_hash=hashlib.sha256(
147
+ payload.model_dump_json().encode()
148
+ ).hexdigest(),
149
+ time_stamp=datetime.now(),
150
+ ),
151
+ )
152
+ return NEXT_USER_MESSAGE_JINJA2_TEMPLATE.render(
153
+ api_call_as_json=api_call.model_dump_json(indent=2)
154
+ )
155
+
156
+ def create_assistant_confirmation_message(self, *, payload: PayloadType) -> str:
157
+ return ASSISTANT_CONFIRMATION_MESSAGE_JINJA2_TEMPLATE.render(
158
+ api_call_as_markdown_table=dict_to_markdown_table(payload.model_dump()),
159
+ button_text="Please confirm the call by pressing this button.",
160
+ next_user_message=self._create_next_user_message(payload),
161
+ )
162
+
163
+ def call_api(
164
+ self,
165
+ *,
166
+ headers: dict[str, str],
167
+ path_params: PathParamsType,
168
+ payload: PayloadType,
169
+ ) -> ResponseType:
170
+ params = path_params.model_dump()
171
+ params.update(payload.model_dump())
172
+
173
+ response = self._requestor.request(
174
+ headers=headers,
175
+ **params,
176
+ )
177
+ return self._operation.handle_response(response)
178
+
179
+
180
+ if __name__ == "__main__":
181
+ import logging
182
+ from string import Template
183
+
184
+ from unique_toolkit._common.endpoint_builder import (
185
+ EndpointMethods,
186
+ build_api_operation,
187
+ )
188
+
189
+ class GetUserPathParams(BaseModel):
190
+ user_id: int
191
+
192
+ class GetUserRequestBody(BaseModel):
193
+ include_profile: bool = False
194
+
195
+ class UserResponse(BaseModel):
196
+ id: int
197
+ name: str
198
+
199
+ class CombinedParams(GetUserPathParams, GetUserRequestBody):
200
+ pass
201
+
202
+ UserEndpoint = build_api_operation(
203
+ method=EndpointMethods.GET,
204
+ url_template=Template("https://api.example.com/users/{user_id}"),
205
+ path_params_constructor=GetUserPathParams,
206
+ payload_constructor=GetUserRequestBody,
207
+ response_model_type=UserResponse,
208
+ )
209
+
210
+ human_verification_manager = HumanVerificationManagerForApiCalling(
211
+ logger=logging.getLogger(__name__),
212
+ operation=UserEndpoint,
213
+ requestor_type=RequestorType.FAKE,
214
+ return_value={"id": 100, "name": "John Doe"},
215
+ )
216
+
217
+ payload = GetUserRequestBody(include_profile=True)
218
+
219
+ api_call = human_verification_manager._api_call_model(
220
+ payload=payload,
221
+ confirmation=HumanConfirmation(
222
+ payload_hash=hashlib.sha256(payload.model_dump_json().encode()).hexdigest(),
223
+ time_stamp=datetime.now(),
224
+ ),
225
+ )
226
+
227
+ last_assistant_message = ChatMessage(
228
+ role=ChatMessageRole.ASSISTANT,
229
+ text=api_call.confirmation.payload_hash,
230
+ chat_id="123",
231
+ )
232
+
233
+ user_message_with_api_call = human_verification_manager._create_next_user_message(
234
+ payload=payload
235
+ )
236
+
237
+ print(user_message_with_api_call)
238
+
239
+ payload = human_verification_manager.detect_api_calls_from_user_message(
240
+ user_message=user_message_with_api_call,
241
+ last_assistant_message=last_assistant_message,
242
+ )
243
+
244
+ if payload is None:
245
+ print("❌ Detection failed - payload is None")
246
+ exit(1)
247
+ else:
248
+ print("✅ Detection successful!")
249
+ print(f"Payload: {payload.model_dump()}")
250
+ print("✅ Dict extraction from string works correctly!")
@@ -12,9 +12,11 @@ from typing import (
12
12
  ParamSpec,
13
13
  Protocol,
14
14
  TypeVar,
15
+ cast,
15
16
  )
16
17
 
17
18
  from pydantic import BaseModel
19
+ from typing_extensions import deprecated
18
20
 
19
21
  # Paramspecs
20
22
  PayloadParamSpec = ParamSpec("PayloadParamSpec")
@@ -31,7 +33,7 @@ PayloadType = TypeVar("PayloadType", bound=BaseModel)
31
33
  ModelConstructor = Callable[..., BaseModel]
32
34
 
33
35
 
34
- class EndpointMethods(StrEnum):
36
+ class HttpMethods(StrEnum):
35
37
  GET = "GET"
36
38
  POST = "POST"
37
39
  PUT = "PUT"
@@ -41,8 +43,11 @@ class EndpointMethods(StrEnum):
41
43
  HEAD = "HEAD"
42
44
 
43
45
 
44
- # Necessary for typing of make_endpoint_class
45
- class EndpointClassProtocol(
46
+ # Backward compatibility TODO: Remove in 2.0.0.
47
+ EndpointMethods = HttpMethods
48
+
49
+
50
+ class ApiOperationProtocol(
46
51
  Protocol,
47
52
  Generic[
48
53
  PathParamsSpec,
@@ -52,9 +57,14 @@ class EndpointClassProtocol(
52
57
  ResponseType,
53
58
  ],
54
59
  ):
55
- path_params_model: type[PathParamsType]
56
- payload_model: type[PayloadType]
57
- response_model: type[ResponseType]
60
+ @staticmethod
61
+ def path_params_model() -> type[PathParamsType]: ...
62
+
63
+ @staticmethod
64
+ def payload_model() -> type[PayloadType]: ...
65
+
66
+ @staticmethod
67
+ def response_model() -> type[ResponseType]: ...
58
68
 
59
69
  @staticmethod
60
70
  def create_url(
@@ -78,18 +88,23 @@ class EndpointClassProtocol(
78
88
  @staticmethod
79
89
  def request_method() -> EndpointMethods: ...
80
90
 
91
+ @staticmethod
92
+ def models_from_combined(
93
+ combined: dict[str, Any],
94
+ ) -> tuple[PathParamsType, PayloadType]: ...
95
+
81
96
 
82
97
  # Model for any client to implement
83
- def build_endpoint_class(
98
+ def build_api_operation(
84
99
  *,
85
- method: EndpointMethods,
100
+ method: HttpMethods,
86
101
  url_template: Template,
87
102
  path_params_constructor: Callable[PathParamsSpec, PathParamsType],
88
103
  payload_constructor: Callable[PayloadParamSpec, PayloadType],
89
104
  response_model_type: type[ResponseType],
90
105
  dump_options: dict | None = None,
91
106
  ) -> type[
92
- EndpointClassProtocol[
107
+ ApiOperationProtocol[
93
108
  PathParamsSpec,
94
109
  PathParamsType,
95
110
  PayloadParamSpec,
@@ -107,11 +122,6 @@ def build_endpoint_class(
107
122
  """
108
123
 
109
124
  # Verify that the path_params_constructor and payload_constructor are valid pydantic models
110
- if not isinstance(path_params_constructor, type(BaseModel)):
111
- raise ValueError("path_params_constructor must be a pydantic model")
112
- if not isinstance(payload_constructor, type(BaseModel)):
113
- raise ValueError("payload_constructor must be a pydantic model")
114
-
115
125
  if not dump_options:
116
126
  dump_options = {
117
127
  "exclude_unset": True,
@@ -119,10 +129,18 @@ def build_endpoint_class(
119
129
  "exclude_defaults": True,
120
130
  }
121
131
 
122
- class EndpointClass(EndpointClassProtocol):
123
- path_params_model = path_params_constructor # type: ignore
124
- payload_model = payload_constructor # type: ignore
125
- response_model = response_model_type
132
+ class Operation(ApiOperationProtocol):
133
+ @staticmethod
134
+ def path_params_model() -> type[PathParamsType]:
135
+ return cast(type[PathParamsType], path_params_constructor)
136
+
137
+ @staticmethod
138
+ def payload_model() -> type[PayloadType]:
139
+ return cast(type[PayloadType], payload_constructor)
140
+
141
+ @staticmethod
142
+ def response_model() -> type[ResponseType]:
143
+ return response_model_type
126
144
 
127
145
  @staticmethod
128
146
  def create_url(
@@ -130,7 +148,7 @@ def build_endpoint_class(
130
148
  **kwargs: PathParamsSpec.kwargs,
131
149
  ) -> str:
132
150
  """Create URL from path parameters."""
133
- path_model = EndpointClass.path_params_model(*args, **kwargs)
151
+ path_model = Operation.path_params_model()(*args, **kwargs)
134
152
  path_dict = path_model.model_dump(**dump_options)
135
153
 
136
154
  # Extract expected path parameters from template
@@ -158,7 +176,7 @@ def build_endpoint_class(
158
176
  *args: PayloadParamSpec.args, **kwargs: PayloadParamSpec.kwargs
159
177
  ) -> dict[str, Any]:
160
178
  """Create request body payload."""
161
- request_model = EndpointClass.payload_model(*args, **kwargs)
179
+ request_model = Operation.payload_model()(*args, **kwargs)
162
180
  return request_model.model_dump(**dump_options)
163
181
 
164
182
  @staticmethod
@@ -167,13 +185,58 @@ def build_endpoint_class(
167
185
 
168
186
  @staticmethod
169
187
  def handle_response(response: dict[str, Any]) -> ResponseType:
170
- return EndpointClass.response_model.model_validate(response)
188
+ return Operation.response_model().model_validate(response)
171
189
 
172
190
  @staticmethod
173
- def request_method() -> EndpointMethods:
191
+ def request_method() -> HttpMethods:
174
192
  return method
175
193
 
176
- return EndpointClass
194
+ @staticmethod
195
+ def models_from_combined(
196
+ combined: dict[str, Any],
197
+ ) -> tuple[PathParamsType, PayloadType]:
198
+ path_params = Operation.path_params_model().model_validate(
199
+ combined, by_alias=True, by_name=True
200
+ )
201
+ payload = Operation.payload_model().model_validate(
202
+ combined, by_alias=True, by_name=True
203
+ )
204
+ return path_params, payload
205
+
206
+ return Operation
207
+
208
+
209
+ @deprecated("Use ApiOperationProtocol instead")
210
+ class EndpointClassProtocol(ApiOperationProtocol):
211
+ pass
212
+
213
+
214
+ @deprecated("Use build_api_operation instead")
215
+ def build_endpoint_class(
216
+ *,
217
+ method: HttpMethods,
218
+ url_template: Template,
219
+ path_params_constructor: Callable[PathParamsSpec, PathParamsType],
220
+ payload_constructor: Callable[PayloadParamSpec, PayloadType],
221
+ response_model_type: type[ResponseType],
222
+ dump_options: dict | None = None,
223
+ ) -> type[
224
+ ApiOperationProtocol[
225
+ PathParamsSpec,
226
+ PathParamsType,
227
+ PayloadParamSpec,
228
+ PayloadType,
229
+ ResponseType,
230
+ ]
231
+ ]:
232
+ return build_api_operation(
233
+ method=method,
234
+ url_template=url_template,
235
+ path_params_constructor=path_params_constructor,
236
+ payload_constructor=payload_constructor,
237
+ response_model_type=response_model_type,
238
+ dump_options=dump_options,
239
+ )
177
240
 
178
241
 
179
242
  if __name__ == "__main__":
@@ -195,7 +258,7 @@ if __name__ == "__main__":
195
258
  name: str
196
259
 
197
260
  # Example usage of make_endpoint_class
198
- UserEndpoint = build_endpoint_class(
261
+ UserOperation = build_endpoint_class(
199
262
  url_template=Template("/users/${user_id}"),
200
263
  path_params_constructor=GetUserPathParams,
201
264
  payload_constructor=GetUserRequestBody,
@@ -204,15 +267,15 @@ if __name__ == "__main__":
204
267
  )
205
268
 
206
269
  # Create URL from path parameters
207
- url = UserEndpoint.create_url(user_id=123)
270
+ url = UserOperation.create_url(user_id=123)
208
271
  print(f"URL: {url}")
209
272
 
210
273
  # Create payload from request body parameters
211
- payload = UserEndpoint.create_payload(include_profile=True)
274
+ payload = UserOperation.create_payload(include_profile=True)
212
275
  print(f"Payload: {payload}")
213
276
 
214
277
  # Create response from endpoint
215
- response = UserEndpoint.handle_response(
278
+ response = UserOperation.handle_response(
216
279
  {
217
280
  "id": 123,
218
281
  "name": "John Doe",
@@ -5,8 +5,8 @@ from pydantic import BaseModel
5
5
  from typing_extensions import ParamSpec
6
6
 
7
7
  from unique_toolkit._common.endpoint_builder import (
8
- EndpointClassProtocol,
9
- EndpointMethods,
8
+ ApiOperationProtocol,
9
+ HttpMethods,
10
10
  PathParamsSpec,
11
11
  PathParamsType,
12
12
  PayloadParamSpec,
@@ -21,19 +21,22 @@ CombinedParamsSpec = ParamSpec("CombinedParamsSpec")
21
21
  CombinedParamsType = TypeVar("CombinedParamsType", bound=BaseModel)
22
22
 
23
23
 
24
- class EndpointRequestorProtocol(Protocol, Generic[CombinedParamsSpec, ResponseType]):
24
+ ResponseT_co = TypeVar("ResponseT_co", bound=BaseModel, covariant=True)
25
+
26
+
27
+ class EndpointRequestorProtocol(Protocol, Generic[CombinedParamsSpec, ResponseT_co]):
25
28
  @classmethod
26
29
  def request(
27
30
  cls,
28
31
  headers: dict[str, str],
29
32
  *args: CombinedParamsSpec.args,
30
33
  **kwargs: CombinedParamsSpec.kwargs,
31
- ) -> ResponseType: ...
34
+ ) -> ResponseT_co: ...
32
35
 
33
36
 
34
37
  def build_fake_requestor(
35
- endpoint_type: type[
36
- EndpointClassProtocol[
38
+ operation_type: type[
39
+ ApiOperationProtocol[
37
40
  PathParamsSpec,
38
41
  PathParamsType,
39
42
  PayloadParamSpec,
@@ -45,7 +48,7 @@ def build_fake_requestor(
45
48
  return_value: dict[str, Any],
46
49
  ) -> type[EndpointRequestorProtocol[CombinedParamsSpec, ResponseType]]:
47
50
  class FakeRequestor(EndpointRequestorProtocol):
48
- _endpoint = endpoint_type
51
+ _operation = operation_type
49
52
 
50
53
  @classmethod
51
54
  def request(
@@ -55,20 +58,22 @@ def build_fake_requestor(
55
58
  **kwargs: CombinedParamsSpec.kwargs,
56
59
  ) -> ResponseType:
57
60
  try:
58
- combined_model(*args, **kwargs)
61
+ path_params, payload_model = cls._operation.models_from_combined(
62
+ combined=kwargs
63
+ )
59
64
  except Exception as e:
60
65
  raise ValueError(
61
66
  f"Invalid parameters passed to combined model {combined_model.__name__}: {e}"
62
67
  )
63
68
 
64
- return cls._endpoint.handle_response(return_value)
69
+ return cls._operation.handle_response(return_value)
65
70
 
66
71
  return FakeRequestor
67
72
 
68
73
 
69
74
  def build_request_requestor(
70
- endpoint_type: type[
71
- EndpointClassProtocol[
75
+ operation_type: type[
76
+ ApiOperationProtocol[
72
77
  PathParamsSpec,
73
78
  PathParamsType,
74
79
  PayloadParamSpec,
@@ -77,11 +82,11 @@ def build_request_requestor(
77
82
  ]
78
83
  ],
79
84
  combined_model: Callable[CombinedParamsSpec, CombinedParamsType],
80
- ) -> type[EndpointRequestorProtocol]:
85
+ ) -> type[EndpointRequestorProtocol[CombinedParamsSpec, ResponseType]]:
81
86
  import requests
82
87
 
83
88
  class RequestRequestor(EndpointRequestorProtocol):
84
- _endpoint = endpoint_type
89
+ _operation = operation_type
85
90
 
86
91
  @classmethod
87
92
  def request(
@@ -90,18 +95,21 @@ def build_request_requestor(
90
95
  *args: CombinedParamsSpec.args,
91
96
  **kwargs: CombinedParamsSpec.kwargs,
92
97
  ) -> ResponseType:
93
- url = cls._endpoint.create_url_from_model(combined_model(*args, **kwargs))
94
- payload = cls._endpoint.create_payload_from_model(
95
- combined_model(*args, **kwargs)
98
+ # Create separate instances for path params and payload using endpoint helper
99
+ path_params, payload_model = cls._operation.models_from_combined(
100
+ combined=kwargs
96
101
  )
97
102
 
103
+ url = cls._operation.create_url_from_model(path_params)
104
+ payload = cls._operation.create_payload_from_model(payload_model)
105
+
98
106
  response = requests.request(
99
- method=cls._endpoint.request_method(),
107
+ method=cls._operation.request_method(),
100
108
  url=url,
101
109
  headers=headers,
102
110
  json=payload,
103
111
  )
104
- return cls._endpoint.handle_response(response.json())
112
+ return cls._operation.handle_response(response.json())
105
113
 
106
114
  return RequestRequestor
107
115
 
@@ -113,8 +121,8 @@ class RequestorType(StrEnum):
113
121
 
114
122
  def build_requestor(
115
123
  requestor_type: RequestorType,
116
- endpoint_type: type[
117
- EndpointClassProtocol[
124
+ operation_type: type[
125
+ ApiOperationProtocol[
118
126
  PathParamsSpec,
119
127
  PathParamsType,
120
128
  PayloadParamSpec,
@@ -129,13 +137,13 @@ def build_requestor(
129
137
  match requestor_type:
130
138
  case RequestorType.REQUESTS:
131
139
  return build_request_requestor(
132
- endpoint_type=endpoint_type, combined_model=combined_model
140
+ operation_type=operation_type, combined_model=combined_model
133
141
  )
134
142
  case RequestorType.FAKE:
135
143
  if return_value is None:
136
144
  raise ValueError("return_value is required for fake requestor")
137
145
  return build_fake_requestor(
138
- endpoint_type=endpoint_type,
146
+ operation_type=operation_type,
139
147
  combined_model=combined_model,
140
148
  return_value=return_value,
141
149
  )
@@ -144,7 +152,7 @@ def build_requestor(
144
152
  if __name__ == "__main__":
145
153
  from string import Template
146
154
 
147
- from unique_toolkit._common.endpoint_builder import build_endpoint_class
155
+ from unique_toolkit._common.endpoint_builder import build_api_operation
148
156
 
149
157
  class GetUserPathParams(BaseModel):
150
158
  user_id: int
@@ -159,8 +167,8 @@ if __name__ == "__main__":
159
167
  class CombinedParams(GetUserPathParams, GetUserRequestBody):
160
168
  pass
161
169
 
162
- UserEndpoint = build_endpoint_class(
163
- method=EndpointMethods.GET,
170
+ UserEndpoint = build_api_operation(
171
+ method=HttpMethods.GET,
164
172
  url_template=Template("https://api.example.com/users/{user_id}"),
165
173
  path_params_constructor=GetUserPathParams,
166
174
  payload_constructor=GetUserRequestBody,
@@ -168,7 +176,7 @@ if __name__ == "__main__":
168
176
  )
169
177
 
170
178
  FakeUserRequestor = build_fake_requestor(
171
- endpoint_type=UserEndpoint,
179
+ operation_type=UserEndpoint,
172
180
  combined_model=CombinedParams,
173
181
  return_value={"id": 100, "name": "John Doe"},
174
182
  )
@@ -180,6 +188,16 @@ if __name__ == "__main__":
180
188
  include_profile=True,
181
189
  )
182
190
 
191
+ RequestRequstor = build_request_requestor(
192
+ operation_type=UserEndpoint,
193
+ combined_model=CombinedParams,
194
+ )
195
+
196
+ # Check type hints
197
+ response = RequestRequstor().request(
198
+ headers={"a": "b"}, user_id=123, include_profile=True
199
+ )
200
+
183
201
  print(response.model_dump())
184
202
  print(response.model_json_schema())
185
203
  print(response.id)