unique_toolkit 0.8.24__tar.gz → 0.8.26__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 (124) hide show
  1. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/CHANGELOG.md +9 -0
  2. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/PKG-INFO +11 -1
  3. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/pyproject.toml +2 -1
  4. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/__init__.py +8 -1
  5. unique_toolkit-0.8.26/unique_toolkit/_common/endpoint_builder.py +145 -0
  6. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/app/schemas.py +11 -0
  7. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/app/unique_settings.py +102 -1
  8. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/chat/service.py +4 -0
  9. unique_toolkit-0.8.26/unique_toolkit/framework_utilities/__init__.py +1 -0
  10. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/framework_utilities/langchain/client.py +3 -1
  11. unique_toolkit-0.8.26/unique_toolkit/framework_utilities/openai/__init__.py +6 -0
  12. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/framework_utilities/openai/client.py +4 -1
  13. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/history_manager/history_manager.py +7 -0
  14. unique_toolkit-0.8.26/unique_toolkit/tools/mcp/__init__.py +4 -0
  15. unique_toolkit-0.8.26/unique_toolkit/tools/mcp/manager.py +67 -0
  16. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/tools/mcp/models.py +2 -13
  17. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/tools/mcp/tool_wrapper.py +39 -55
  18. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/tools/schemas.py +4 -1
  19. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/tools/tool.py +10 -4
  20. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/tools/tool_manager.py +1 -3
  21. unique_toolkit-0.8.24/unique_toolkit/tools/mcp/__init__.py +0 -4
  22. unique_toolkit-0.8.24/unique_toolkit/tools/mcp/manager.py +0 -82
  23. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/LICENSE +0 -0
  24. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/README.md +0 -0
  25. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/_common/_base_service.py +0 -0
  26. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/_common/_time_utils.py +0 -0
  27. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/_common/default_language_model.py +0 -0
  28. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/_common/exception.py +0 -0
  29. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/_common/token/image_token_counting.py +0 -0
  30. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/_common/token/token_counting.py +0 -0
  31. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/_common/validate_required_values.py +0 -0
  32. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/_common/validators.py +0 -0
  33. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/app/__init__.py +0 -0
  34. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/app/dev_util.py +0 -0
  35. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/app/init_logging.py +0 -0
  36. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/app/init_sdk.py +0 -0
  37. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/app/performance/async_tasks.py +0 -0
  38. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/app/performance/async_wrapper.py +0 -0
  39. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/app/verification.py +0 -0
  40. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/chat/__init__.py +0 -0
  41. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/chat/constants.py +0 -0
  42. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/chat/functions.py +0 -0
  43. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/chat/schemas.py +0 -0
  44. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/chat/state.py +0 -0
  45. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/chat/utils.py +0 -0
  46. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/content/__init__.py +0 -0
  47. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/content/constants.py +0 -0
  48. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/content/functions.py +0 -0
  49. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/content/schemas.py +0 -0
  50. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/content/service.py +0 -0
  51. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/content/utils.py +0 -0
  52. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/debug_info_manager/debug_info_manager.py +0 -0
  53. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/embedding/__init__.py +0 -0
  54. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/embedding/constants.py +0 -0
  55. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/embedding/functions.py +0 -0
  56. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/embedding/schemas.py +0 -0
  57. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/embedding/service.py +0 -0
  58. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/embedding/utils.py +0 -0
  59. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/evals/config.py +0 -0
  60. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/evals/context_relevancy/prompts.py +0 -0
  61. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/evals/context_relevancy/schema.py +0 -0
  62. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/evals/context_relevancy/service.py +0 -0
  63. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/evals/evaluation_manager.py +0 -0
  64. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/evals/exception.py +0 -0
  65. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/evals/hallucination/constants.py +0 -0
  66. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/evals/hallucination/hallucination_evaluation.py +0 -0
  67. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/evals/hallucination/prompts.py +0 -0
  68. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/evals/hallucination/service.py +0 -0
  69. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/evals/hallucination/utils.py +0 -0
  70. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/evals/output_parser.py +0 -0
  71. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/evals/schemas.py +0 -0
  72. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/evals/tests/test_context_relevancy_service.py +0 -0
  73. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/evals/tests/test_output_parser.py +0 -0
  74. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/evaluators/__init__.py +0 -0
  75. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/evaluators/config.py +0 -0
  76. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/evaluators/constants.py +0 -0
  77. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/evaluators/context_relevancy/constants.py +0 -0
  78. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/evaluators/context_relevancy/prompts.py +0 -0
  79. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/evaluators/context_relevancy/service.py +0 -0
  80. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/evaluators/context_relevancy/utils.py +0 -0
  81. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/evaluators/exception.py +0 -0
  82. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/evaluators/hallucination/constants.py +0 -0
  83. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/evaluators/hallucination/prompts.py +0 -0
  84. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/evaluators/hallucination/service.py +0 -0
  85. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/evaluators/hallucination/utils.py +0 -0
  86. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/evaluators/output_parser.py +0 -0
  87. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/evaluators/schemas.py +0 -0
  88. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/framework_utilities/langchain/history.py +0 -0
  89. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/framework_utilities/openai/message_builder.py +0 -0
  90. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/framework_utilities/utils.py +0 -0
  91. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/history_manager/history_construction_with_contents.py +0 -0
  92. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/history_manager/loop_token_reducer.py +0 -0
  93. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/history_manager/utils.py +0 -0
  94. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/language_model/__init__.py +0 -0
  95. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/language_model/builder.py +0 -0
  96. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/language_model/constants.py +0 -0
  97. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/language_model/functions.py +0 -0
  98. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/language_model/infos.py +0 -0
  99. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/language_model/prompt.py +0 -0
  100. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/language_model/reference.py +0 -0
  101. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/language_model/schemas.py +0 -0
  102. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/language_model/service.py +0 -0
  103. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/language_model/utils.py +0 -0
  104. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/postprocessor/postprocessor_manager.py +0 -0
  105. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/protocols/support.py +0 -0
  106. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/reference_manager/reference_manager.py +0 -0
  107. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/short_term_memory/__init__.py +0 -0
  108. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/short_term_memory/constants.py +0 -0
  109. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/short_term_memory/functions.py +0 -0
  110. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/short_term_memory/persistent_short_term_memory_manager.py +0 -0
  111. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/short_term_memory/schemas.py +0 -0
  112. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/short_term_memory/service.py +0 -0
  113. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/smart_rules/__init__.py +0 -0
  114. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/smart_rules/compile.py +0 -0
  115. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/thinking_manager/thinking_manager.py +0 -0
  116. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/tools/config.py +0 -0
  117. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/tools/factory.py +0 -0
  118. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/tools/test/test_mcp_manager.py +0 -0
  119. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/tools/test/test_tool_progress_reporter.py +0 -0
  120. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/tools/tool_progress_reporter.py +0 -0
  121. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/tools/utils/execution/execution.py +0 -0
  122. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/tools/utils/source_handling/schema.py +0 -0
  123. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/tools/utils/source_handling/source_formatting.py +0 -0
  124. {unique_toolkit-0.8.24 → unique_toolkit-0.8.26}/unique_toolkit/tools/utils/source_handling/tests/test_source_formatting.py +0 -0
@@ -5,6 +5,15 @@ 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
+ ## [0.8.26] - 2025-08-27
9
+ - Optimized MCP manager
10
+
11
+ ## [0.8.25] - 2025-08-27
12
+ - Load environment variables automatically from plattform dirs or environment
13
+ - General Endpoint definition utility
14
+ - Expose `LanguageModelToolDescription` and `LanguageModelName` directly
15
+ - Get initial debug information from chat payload
16
+
8
17
  ## [0.8.24] - 2025-08-25
9
18
  - Optimized hallucination manager
10
19
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: unique_toolkit
3
- Version: 0.8.24
3
+ Version: 0.8.26
4
4
  Summary:
5
5
  License: Proprietary
6
6
  Author: Martin Fadler
@@ -12,6 +12,7 @@ Classifier: Programming Language :: Python :: 3.11
12
12
  Classifier: Programming Language :: Python :: 3.12
13
13
  Requires-Dist: numpy (>=1.26.4,<2.0.0)
14
14
  Requires-Dist: openai (>=1.99.9,<2.0.0)
15
+ Requires-Dist: platformdirs (>=4.0.0,<5.0.0)
15
16
  Requires-Dist: pydantic (>=2.8.2,<3.0.0)
16
17
  Requires-Dist: pydantic-settings (>=2.10.1,<3.0.0)
17
18
  Requires-Dist: pyhumps (>=3.8.0,<4.0.0)
@@ -114,6 +115,15 @@ All notable changes to this project will be documented in this file.
114
115
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
115
116
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
116
117
 
118
+ ## [0.8.26] - 2025-08-27
119
+ - Optimized MCP manager
120
+
121
+ ## [0.8.25] - 2025-08-27
122
+ - Load environment variables automatically from plattform dirs or environment
123
+ - General Endpoint definition utility
124
+ - Expose `LanguageModelToolDescription` and `LanguageModelName` directly
125
+ - Get initial debug information from chat payload
126
+
117
127
  ## [0.8.24] - 2025-08-25
118
128
  - Optimized hallucination manager
119
129
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "unique_toolkit"
3
- version = "0.8.24"
3
+ version = "0.8.26"
4
4
  description = ""
5
5
  authors = [
6
6
  "Martin Fadler <martin.fadler@unique.ch>",
@@ -24,6 +24,7 @@ unique-sdk = "^0.10.0"
24
24
  pydantic-settings = "^2.10.1"
25
25
  sseclient = "^0.0.27"
26
26
  openai = "^1.99.9"
27
+ platformdirs = "^4.0.0"
27
28
 
28
29
  [tool.poetry.group.langchain]
29
30
  optional = true
@@ -2,7 +2,12 @@
2
2
  from unique_toolkit.chat import ChatService
3
3
  from unique_toolkit.content import ContentService
4
4
  from unique_toolkit.embedding import EmbeddingService
5
- from unique_toolkit.language_model import LanguageModelMessages, LanguageModelService
5
+ from unique_toolkit.language_model import (
6
+ LanguageModelMessages,
7
+ LanguageModelName,
8
+ LanguageModelService,
9
+ LanguageModelToolDescription,
10
+ )
6
11
  from unique_toolkit.short_term_memory import ShortTermMemoryService
7
12
 
8
13
  # You can add other classes you frequently use here as well
@@ -10,6 +15,8 @@ from unique_toolkit.short_term_memory import ShortTermMemoryService
10
15
  __all__ = [
11
16
  "LanguageModelService",
12
17
  "LanguageModelMessages",
18
+ "LanguageModelName",
19
+ "LanguageModelToolDescription",
13
20
  "ChatService",
14
21
  "ContentService",
15
22
  "EmbeddingService",
@@ -0,0 +1,145 @@
1
+ """
2
+ This module provides a minimal framework for building endpoint classes such that a client can use
3
+ the endpoints without having to know the details of the endpoints.
4
+ """
5
+
6
+ from collections.abc import Callable
7
+ from string import Formatter, Template
8
+ from typing import (
9
+ Any,
10
+ Generic,
11
+ ParamSpec,
12
+ Protocol,
13
+ TypeVar,
14
+ )
15
+
16
+ from pydantic import BaseModel
17
+
18
+ # Type variables
19
+ ResponseType = TypeVar("ResponseType", bound=BaseModel)
20
+ PathParamsType = TypeVar("PathParamsType", bound=BaseModel)
21
+ RequestBodyType = TypeVar("RequestBodyType", bound=BaseModel)
22
+
23
+ # ParamSpecs for function signatures
24
+ RequestConstructorSpec = ParamSpec("RequestConstructorSpec")
25
+ PathParamsSpec = ParamSpec("PathParamsSpec")
26
+ RequestBodySpec = ParamSpec("RequestBodySpec")
27
+
28
+
29
+ # Necessary for typing of make_endpoint_class
30
+ class EndpointClassProtocol(Protocol, Generic[PathParamsSpec, RequestBodySpec]):
31
+ @staticmethod
32
+ def create_url(
33
+ *args: PathParamsSpec.args, **kwargs: PathParamsSpec.kwargs
34
+ ) -> str: ...
35
+
36
+ @staticmethod
37
+ def create_payload(
38
+ *args: RequestBodySpec.args, **kwargs: RequestBodySpec.kwargs
39
+ ) -> dict[str, Any]: ...
40
+
41
+
42
+ # Model for any client to implement
43
+ class Client(Protocol):
44
+ def request(
45
+ self,
46
+ endpoint: EndpointClassProtocol,
47
+ ) -> dict[str, Any]: ...
48
+
49
+
50
+ def build_endpoint_class(
51
+ *,
52
+ url_template: Template,
53
+ path_params_model: Callable[PathParamsSpec, PathParamsType],
54
+ payload_model: Callable[RequestBodySpec, RequestBodyType],
55
+ response_model: type[ResponseType],
56
+ dump_options: dict | None = None,
57
+ ) -> type[EndpointClassProtocol[PathParamsSpec, RequestBodySpec]]:
58
+ """Generate a class with static methods for endpoint handling.
59
+
60
+ Uses separate models for path parameters and request body for clean API design.
61
+
62
+ Returns a class with static methods:
63
+ - create_url: Creates URL from path parameters
64
+ - create_payload: Creates request body payload
65
+ """
66
+ if not dump_options:
67
+ dump_options = {
68
+ "exclude_unset": True,
69
+ "by_alias": True,
70
+ "exclude_defaults": True,
71
+ }
72
+
73
+ class EndpointClass(EndpointClassProtocol):
74
+ @staticmethod
75
+ def create_url(
76
+ *args: PathParamsSpec.args, **kwargs: PathParamsSpec.kwargs
77
+ ) -> str:
78
+ """Create URL from path parameters."""
79
+ path_model = path_params_model(*args, **kwargs)
80
+ path_dict = path_model.model_dump(**dump_options)
81
+
82
+ # Extract expected path parameters from template
83
+ template_params = [
84
+ fname
85
+ for _, fname, _, _ in Formatter().parse(url_template.template)
86
+ if fname is not None
87
+ ]
88
+
89
+ # Verify all required path parameters are present
90
+ missing_params = [
91
+ param for param in template_params if param not in path_dict
92
+ ]
93
+ if missing_params:
94
+ raise ValueError(f"Missing path parameters: {missing_params}")
95
+
96
+ return url_template.substitute(**path_dict)
97
+
98
+ @staticmethod
99
+ def create_payload(
100
+ *args: RequestBodySpec.args, **kwargs: RequestBodySpec.kwargs
101
+ ) -> dict[str, Any]:
102
+ """Create request body payload."""
103
+ request_model = payload_model(*args, **kwargs)
104
+ return request_model.model_dump(**dump_options)
105
+
106
+ @staticmethod
107
+ def handle_response(response: dict[str, Any]) -> ResponseType:
108
+ return response_model.model_validate(response)
109
+
110
+ return EndpointClass
111
+
112
+
113
+ if __name__ == "__main__":
114
+ # Example models
115
+ class GetUserPathParams(BaseModel):
116
+ """Path parameters for the user endpoint."""
117
+
118
+ user_id: int
119
+
120
+ class GetUserRequestBody(BaseModel):
121
+ """Request body/query parameters for the user endpoint."""
122
+
123
+ include_profile: bool = False
124
+
125
+ class UserResponse(BaseModel):
126
+ """Response model for user data."""
127
+
128
+ id: int
129
+ name: str
130
+
131
+ # Example usage of make_endpoint_class
132
+ UserEndpoint = build_endpoint_class(
133
+ url_template=Template("/users/${user_id}"),
134
+ path_params_model=GetUserPathParams,
135
+ payload_model=GetUserRequestBody,
136
+ response_model=UserResponse,
137
+ )
138
+
139
+ # Create URL from path parameters
140
+ url = UserEndpoint.create_url(user_id=123)
141
+ print(f"URL: {url}")
142
+
143
+ # Create payload from request body parameters
144
+ payload = UserEndpoint.create_payload(include_profile=True)
145
+ print(f"Payload: {payload}")
@@ -227,6 +227,17 @@ class ChatEvent(BaseEvent):
227
227
  data = json.load(f)
228
228
  return cls.model_validate(data)
229
229
 
230
+ def get_initial_debug_info(self) -> dict[str, Any]:
231
+ """Get the debug information for the chat event"""
232
+
233
+ # TODO: Make sure this coincides with what is shown in the first user message
234
+ return {
235
+ "user_metadata": self.payload.user_metadata,
236
+ "tool_parameters": self.payload.tool_parameters,
237
+ "chosen_module": self.payload.name,
238
+ "assistant": {"id": self.payload.assistant_id},
239
+ }
240
+
230
241
 
231
242
  @deprecated(
232
243
  """Use the more specific `ChatEvent` instead that has the same properties. \
@@ -1,8 +1,11 @@
1
+ import os
1
2
  from logging import getLogger
2
3
  from pathlib import Path
3
4
  from typing import Self, TypeVar
4
5
  from urllib.parse import urlparse, urlunparse
5
6
 
7
+ import unique_sdk
8
+ from platformdirs import user_config_dir
6
9
  from pydantic import AliasChoices, Field, SecretStr, model_validator
7
10
  from pydantic_settings import BaseSettings, SettingsConfigDict
8
11
 
@@ -100,7 +103,9 @@ class UniqueApi(BaseSettings):
100
103
  parsed = urlparse(self.base_url)
101
104
 
102
105
  path = "/public/chat"
103
- if parsed.hostname and "qa.unique" in parsed.hostname:
106
+ if parsed.hostname and (
107
+ "qa.unique" in parsed.hostname or ".unique" in parsed.hostname
108
+ ):
104
109
  path = "/public/chat-gen2"
105
110
  return urlunparse(parsed._replace(path=path, query=None, fragment=None))
106
111
 
@@ -142,12 +147,57 @@ class UniqueAuth(BaseSettings):
142
147
  return warn_about_defaults(self)
143
148
 
144
149
 
150
+ class EnvFileNotFoundError(FileNotFoundError):
151
+ """Raised when no environment file can be found in any of the expected locations."""
152
+
153
+
145
154
  class UniqueSettings:
146
155
  def __init__(self, auth: UniqueAuth, app: UniqueApp, api: UniqueApi):
147
156
  self.app = app
148
157
  self.auth = auth
149
158
  self.api = api
150
159
 
160
+ @classmethod
161
+ def _find_env_file(cls, filename: str = "unique.env") -> Path:
162
+ """Find environment file using cross-platform fallback locations.
163
+
164
+ Search order:
165
+ 1. UNIQUE_ENV_FILE environment variable
166
+ 2. Current working directory
167
+ 3. User config directory (cross-platform via platformdirs)
168
+
169
+ Args:
170
+ filename: Name of the environment file (default: 'unique.env')
171
+
172
+ Returns:
173
+ Path to the environment file.
174
+
175
+ Raises:
176
+ EnvFileNotFoundError: If no environment file is found in any location.
177
+ """
178
+ locations = [
179
+ # 1. Explicit environment variable
180
+ Path(env_path) if (env_path := os.environ.get("UNIQUE_ENV_FILE")) else None,
181
+ # 2. Current working directory
182
+ Path.cwd() / filename,
183
+ # 3. User config directory (cross-platform)
184
+ Path(user_config_dir("unique", "unique-toolkit")) / filename,
185
+ ]
186
+
187
+ for location in locations:
188
+ if location and location.exists() and location.is_file():
189
+ return location
190
+
191
+ # If no file found, provide helpful error message
192
+ searched_locations = [str(loc) for loc in locations if loc is not None]
193
+ raise EnvFileNotFoundError(
194
+ f"Environment file '{filename}' not found. Searched locations:\n"
195
+ + "\n".join(f" - {loc}" for loc in searched_locations)
196
+ + "\n\nTo fix this:\n"
197
+ + f" 1. Create {filename} in one of the above locations, or\n"
198
+ + f" 2. Set UNIQUE_ENV_FILE environment variable to point to your {filename} file"
199
+ )
200
+
151
201
  @classmethod
152
202
  def from_env(cls, env_file: Path | None = None) -> "UniqueSettings":
153
203
  """Initialize settings from environment variables and/or env file.
@@ -171,3 +221,54 @@ class UniqueSettings:
171
221
  app = UniqueApp(_env_file=env_file_str) # type: ignore[call-arg]
172
222
  api = UniqueApi(_env_file=env_file_str) # type: ignore[call-arg]
173
223
  return cls(auth=auth, app=app, api=api)
224
+
225
+ @classmethod
226
+ def from_env_auto(cls, filename: str = "unique.env") -> "UniqueSettings":
227
+ """Initialize settings by automatically finding environment file.
228
+
229
+ This method will automatically search for an environment file in standard locations
230
+ and fall back to environment variables only if no file is found.
231
+
232
+ Args:
233
+ filename: Name of the environment file to search for (default: '.env')
234
+
235
+ Returns:
236
+ UniqueSettings instance with values loaded from found env file or environment variables.
237
+ """
238
+ try:
239
+ env_file = cls._find_env_file(filename)
240
+ return cls.from_env(env_file=env_file)
241
+ except EnvFileNotFoundError:
242
+ logger.warning(
243
+ f"Environment file '{filename}' not found. Falling back to environment variables only."
244
+ )
245
+ # Fall back to environment variables only
246
+ return cls.from_env()
247
+
248
+ def init_sdk(self) -> None:
249
+ """Initialize the unique_sdk global configuration with these settings.
250
+
251
+ This method configures the global unique_sdk module with the API key,
252
+ app ID, and base URL from these settings.
253
+ """
254
+ unique_sdk.api_key = self.app.key.get_secret_value()
255
+ unique_sdk.app_id = self.app.id.get_secret_value()
256
+ unique_sdk.api_base = self.api.sdk_url()
257
+
258
+ @classmethod
259
+ def from_env_auto_with_sdk_init(
260
+ cls, filename: str = "unique.env"
261
+ ) -> "UniqueSettings":
262
+ """Initialize settings and SDK in one convenient call.
263
+
264
+ This method combines from_env_auto() and init_sdk() for the most common use case.
265
+
266
+ Args:
267
+ filename: Name of the environment file to search for (default: '.env')
268
+
269
+ Returns:
270
+ UniqueSettings instance with SDK already initialized.
271
+ """
272
+ settings = cls.from_env_auto(filename)
273
+ settings.init_sdk()
274
+ return settings
@@ -479,6 +479,10 @@ class ChatService:
479
479
  set_completed_at=set_completed_at or False,
480
480
  )
481
481
 
482
+ def free_user_input(self) -> None:
483
+ """Unblocks the next user input"""
484
+ self.modify_assistant_message(set_completed_at=True)
485
+
482
486
  def get_full_history(self) -> list[ChatMessage]:
483
487
  """Loads the full chat history for the chat session synchronously.
484
488
 
@@ -0,0 +1 @@
1
+ """Framework utilities for integrating with external frameworks."""
@@ -23,7 +23,7 @@ else:
23
23
 
24
24
 
25
25
  def get_client(
26
- unique_settings: UniqueSettings, model: str = "AZURE_GPT_4o_2024_0806"
26
+ unique_settings: UniqueSettings | None = None, model: str = "AZURE_GPT_4o_2024_0806"
27
27
  ) -> ChatOpenAI:
28
28
  """Get a Langchain ChatOpenAI client instance.
29
29
 
@@ -36,6 +36,8 @@ def get_client(
36
36
  Raises:
37
37
  LangchainNotInstalledError: If langchain-openai package is not installed
38
38
  """
39
+ if unique_settings is None:
40
+ unique_settings = UniqueSettings.from_env_auto()
39
41
 
40
42
  return ChatOpenAI(
41
43
  base_url=unique_settings.api.openai_proxy_url(),
@@ -0,0 +1,6 @@
1
+ """OpenAI framework utilities."""
2
+
3
+ from .client import get_openai_client
4
+ from .message_builder import OpenAIMessageBuilder
5
+
6
+ __all__ = ["get_openai_client", "OpenAIMessageBuilder"]
@@ -22,7 +22,7 @@ else:
22
22
  raise OpenAINotInstalledError()
23
23
 
24
24
 
25
- def get_openai_client(unique_settings: UniqueSettings) -> OpenAI:
25
+ def get_openai_client(unique_settings: UniqueSettings | None = None) -> OpenAI:
26
26
  """Get an OpenAI client instance.
27
27
 
28
28
  Args:
@@ -34,6 +34,9 @@ def get_openai_client(unique_settings: UniqueSettings) -> OpenAI:
34
34
  Raises:
35
35
  OpenAINotInstalledError: If OpenAI package is not installed
36
36
  """
37
+ if unique_settings is None:
38
+ unique_settings = UniqueSettings.from_env_auto()
39
+
37
40
  default_headers = get_default_headers(unique_settings.app, unique_settings.auth)
38
41
 
39
42
  return OpenAI(
@@ -158,6 +158,13 @@ class HistoryManager:
158
158
  f"Appending tool call result to history: {tool_response.name}"
159
159
  )
160
160
 
161
+ if tool_response.content != "":
162
+ return LanguageModelToolMessage(
163
+ content=tool_response.content,
164
+ tool_call_id=tool_response.id, # type: ignore
165
+ name=tool_response.name,
166
+ )
167
+
161
168
  content_chunks = (
162
169
  tool_response.content_chunks or []
163
170
  ) # it can be that the tool response does not have content chunks
@@ -0,0 +1,4 @@
1
+ from .models import MCPToolConfig
2
+ from .tool_wrapper import MCPToolWrapper
3
+
4
+ __all__ = ["MCPToolWrapper", "MCPToolConfig"]
@@ -0,0 +1,67 @@
1
+ import logging
2
+
3
+ from unique_toolkit.tools.config import ToolBuildConfig, ToolIcon, ToolSelectionPolicy
4
+ from unique_toolkit.app.schemas import ChatEvent, McpServer
5
+ from unique_toolkit.tools.schemas import BaseToolConfig
6
+ from unique_toolkit.tools.tool import Tool
7
+ from unique_toolkit.tools.mcp.models import MCPToolConfig
8
+ from unique_toolkit.tools.mcp.tool_wrapper import MCPToolWrapper
9
+ from unique_toolkit.tools.tool_progress_reporter import ToolProgressReporter
10
+
11
+
12
+ class MCPManager:
13
+ def __init__(
14
+ self,
15
+ mcp_servers: list[McpServer],
16
+ event: ChatEvent,
17
+ tool_progress_reporter: ToolProgressReporter,
18
+ ):
19
+ self._mcp_servers = mcp_servers
20
+ self._event = event
21
+ self._tool_progress_reporter = tool_progress_reporter
22
+
23
+ def get_mcp_servers(self):
24
+ return self._mcp_servers
25
+
26
+ def get_mcp_server_by_id(self, id: str):
27
+ return next((server for server in self._mcp_servers if server.id == id), None)
28
+
29
+ def get_all_mcp_tools(self) -> list[Tool[BaseToolConfig]]:
30
+ selected_tools = []
31
+ for server in self._mcp_servers:
32
+ if not hasattr(server, "tools"):
33
+ continue
34
+ if not server.tools:
35
+ continue
36
+
37
+ for tool in server.tools:
38
+ try:
39
+ config = MCPToolConfig(
40
+ server_id=server.id,
41
+ server_name=server.name,
42
+ server_system_prompt=server.system_prompt,
43
+ server_user_prompt=server.user_prompt,
44
+ mcp_source_id=server.id,
45
+ )
46
+ wrapper = MCPToolWrapper(
47
+ mcp_server=server,
48
+ mcp_tool=tool,
49
+ config=config,
50
+ event=self._event,
51
+ tool_progress_reporter=self._tool_progress_reporter,
52
+ )
53
+ wrapper.settings = ToolBuildConfig( # TODO: this must be refactored to behave like the other tools.
54
+ name=tool.name,
55
+ configuration=config,
56
+ display_name=tool.title or tool.name,
57
+ is_exclusive=False,
58
+ is_enabled=True,
59
+ icon=ToolIcon.BOOK,
60
+ selection_policy=ToolSelectionPolicy.BY_USER,
61
+ )
62
+ selected_tools.append(wrapper)
63
+ except Exception as e:
64
+ logging.error(
65
+ f"Error creating MCP tool wrapper for {tool.name}: {e}"
66
+ )
67
+ return selected_tools
@@ -1,9 +1,8 @@
1
- from typing import Any, Dict, Optional, Protocol
2
-
1
+ from typing import Any, Dict, Optional
3
2
  from unique_toolkit.tools.schemas import BaseToolConfig
4
3
 
5
4
 
6
- class MCPTool(Protocol):
5
+ class MCPTool:
7
6
  """Protocol defining the expected structure of an MCP tool."""
8
7
 
9
8
  name: str
@@ -18,16 +17,6 @@ class MCPTool(Protocol):
18
17
  is_connected: bool
19
18
 
20
19
 
21
- class EnrichedMCPTool(MCPTool, Protocol):
22
- """Protocol for MCP tools enriched with server information."""
23
-
24
- server_id: str
25
- server_name: str
26
- server_system_prompt: Optional[str]
27
- server_user_prompt: Optional[str]
28
- mcp_source_id: str
29
-
30
-
31
20
  class MCPToolConfig(BaseToolConfig):
32
21
  """Configuration for MCP tools"""
33
22