unique_toolkit 0.8.3__tar.gz → 0.8.5__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 (91) hide show
  1. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/CHANGELOG.md +6 -0
  2. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/PKG-INFO +7 -1
  3. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/pyproject.toml +1 -1
  4. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/app/schemas.py +3 -0
  5. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/app/unique_settings.py +56 -17
  6. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/framework_utilities/langchain/history.py +2 -2
  7. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/framework_utilities/openai/client.py +1 -1
  8. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/language_model/schemas.py +18 -0
  9. unique_toolkit-0.8.5/unique_toolkit/reference_manager/reference_manager.py +72 -0
  10. unique_toolkit-0.8.5/unique_toolkit/tools/agent_chunks_handler.py +62 -0
  11. unique_toolkit-0.8.5/unique_toolkit/tools/config.py +108 -0
  12. unique_toolkit-0.8.3/unique_toolkit/tools/tool_factory.py → unique_toolkit-0.8.5/unique_toolkit/tools/factory.py +15 -5
  13. unique_toolkit-0.8.5/unique_toolkit/tools/schemas.py +138 -0
  14. unique_toolkit-0.8.5/unique_toolkit/tools/test/test_tool_progress_reporter.py +204 -0
  15. unique_toolkit-0.8.5/unique_toolkit/tools/tool.py +168 -0
  16. unique_toolkit-0.8.5/unique_toolkit/tools/tool_manager.py +242 -0
  17. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/tools/tool_progress_reporter.py +4 -11
  18. unique_toolkit-0.8.5/unique_toolkit/tools/utils/execution/execution.py +282 -0
  19. unique_toolkit-0.8.5/unique_toolkit/tools/utils/source_handling/schema.py +22 -0
  20. unique_toolkit-0.8.5/unique_toolkit/tools/utils/source_handling/source_formatting.py +207 -0
  21. unique_toolkit-0.8.5/unique_toolkit/tools/utils/source_handling/tests/test_source_formatting.py +215 -0
  22. unique_toolkit-0.8.3/unique_toolkit/tools/tool_definitions.py +0 -145
  23. unique_toolkit-0.8.3/unique_toolkit/tools/tool_definitionsV2.py +0 -137
  24. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/LICENSE +0 -0
  25. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/README.md +0 -0
  26. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/__init__.py +0 -0
  27. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/_common/_base_service.py +0 -0
  28. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/_common/_time_utils.py +0 -0
  29. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/_common/exception.py +0 -0
  30. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/_common/validate_required_values.py +0 -0
  31. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/_common/validators.py +0 -0
  32. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/app/__init__.py +0 -0
  33. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/app/dev_util.py +0 -0
  34. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/app/init_logging.py +0 -0
  35. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/app/init_sdk.py +0 -0
  36. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/app/performance/async_tasks.py +0 -0
  37. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/app/performance/async_wrapper.py +0 -0
  38. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/app/verification.py +0 -0
  39. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/chat/__init__.py +0 -0
  40. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/chat/constants.py +0 -0
  41. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/chat/functions.py +0 -0
  42. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/chat/schemas.py +0 -0
  43. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/chat/service.py +0 -0
  44. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/chat/state.py +0 -0
  45. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/chat/utils.py +0 -0
  46. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/content/__init__.py +0 -0
  47. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/content/constants.py +0 -0
  48. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/content/functions.py +0 -0
  49. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/content/schemas.py +0 -0
  50. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/content/service.py +0 -0
  51. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/content/utils.py +0 -0
  52. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/embedding/__init__.py +0 -0
  53. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/embedding/constants.py +0 -0
  54. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/embedding/functions.py +0 -0
  55. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/embedding/schemas.py +0 -0
  56. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/embedding/service.py +0 -0
  57. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/embedding/utils.py +0 -0
  58. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/evaluators/__init__.py +0 -0
  59. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/evaluators/config.py +0 -0
  60. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/evaluators/constants.py +0 -0
  61. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/evaluators/context_relevancy/constants.py +0 -0
  62. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/evaluators/context_relevancy/prompts.py +0 -0
  63. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/evaluators/context_relevancy/service.py +0 -0
  64. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/evaluators/context_relevancy/utils.py +0 -0
  65. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/evaluators/exception.py +0 -0
  66. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/evaluators/hallucination/constants.py +0 -0
  67. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/evaluators/hallucination/prompts.py +0 -0
  68. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/evaluators/hallucination/service.py +0 -0
  69. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/evaluators/hallucination/utils.py +0 -0
  70. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/evaluators/output_parser.py +0 -0
  71. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/evaluators/schemas.py +0 -0
  72. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/framework_utilities/langchain/client.py +0 -0
  73. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/framework_utilities/openai/message_builder.py +0 -0
  74. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/framework_utilities/utils.py +0 -0
  75. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/language_model/__init__.py +0 -0
  76. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/language_model/builder.py +0 -0
  77. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/language_model/constants.py +0 -0
  78. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/language_model/functions.py +0 -0
  79. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/language_model/infos.py +0 -0
  80. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/language_model/prompt.py +0 -0
  81. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/language_model/reference.py +0 -0
  82. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/language_model/service.py +0 -0
  83. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/language_model/utils.py +0 -0
  84. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/protocols/support.py +0 -0
  85. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/short_term_memory/__init__.py +0 -0
  86. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/short_term_memory/constants.py +0 -0
  87. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/short_term_memory/functions.py +0 -0
  88. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/short_term_memory/schemas.py +0 -0
  89. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/short_term_memory/service.py +0 -0
  90. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/smart_rules/__init__.py +0 -0
  91. {unique_toolkit-0.8.3 → unique_toolkit-0.8.5}/unique_toolkit/smart_rules/compile.py +0 -0
@@ -5,6 +5,12 @@ 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.5] - 2025-08-06
9
+ - Refactored tools to be in the tool-kit
10
+
11
+ ## [0.8.4] - 2025-08-06
12
+ - Make unique settings compatible with legacy environment variables
13
+
8
14
  ## [0.8.3] - 2025-08-05
9
15
  - Expose threshold field for search.
10
16
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: unique_toolkit
3
- Version: 0.8.3
3
+ Version: 0.8.5
4
4
  Summary:
5
5
  License: Proprietary
6
6
  Author: Martin Fadler
@@ -113,6 +113,12 @@ All notable changes to this project will be documented in this file.
113
113
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
114
114
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
115
115
 
116
+ ## [0.8.5] - 2025-08-06
117
+ - Refactored tools to be in the tool-kit
118
+
119
+ ## [0.8.4] - 2025-08-06
120
+ - Make unique settings compatible with legacy environment variables
121
+
116
122
  ## [0.8.3] - 2025-08-05
117
123
  - Expose threshold field for search.
118
124
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "unique_toolkit"
3
- version = "0.8.3"
3
+ version = "0.8.5"
4
4
  description = ""
5
5
  authors = [
6
6
  "Martin Fadler <martin.fadler@unique.ch>",
@@ -51,6 +51,7 @@ class BaseEvent(BaseModel):
51
51
  # MCP schemas
52
52
  ###
53
53
 
54
+
54
55
  class McpTool(BaseModel):
55
56
  model_config = model_config
56
57
 
@@ -79,6 +80,7 @@ class McpTool(BaseModel):
79
80
  description="Whether the tool is connected to the MCP server. This is a Unique specific field.",
80
81
  )
81
82
 
83
+
82
84
  class McpServer(BaseModel):
83
85
  model_config = model_config
84
86
 
@@ -94,6 +96,7 @@ class McpServer(BaseModel):
94
96
  )
95
97
  tools: list[McpTool] = []
96
98
 
99
+
97
100
  ###
98
101
  # ChatEvent schemas
99
102
  ###
@@ -3,7 +3,7 @@ from pathlib import Path
3
3
  from typing import Self, TypeVar
4
4
  from urllib.parse import urlparse, urlunparse
5
5
 
6
- from pydantic import Field, SecretStr, model_validator
6
+ from pydantic import AliasChoices, Field, SecretStr, model_validator
7
7
  from pydantic_settings import BaseSettings, SettingsConfigDict
8
8
 
9
9
  logger = getLogger(__name__)
@@ -13,18 +13,34 @@ T = TypeVar("T", bound=BaseSettings)
13
13
 
14
14
  def warn_about_defaults(instance: T) -> T:
15
15
  """Log warnings for fields that are using default values."""
16
- for field_name, model_field in instance.model_fields.items():
16
+ for field_name, model_field in instance.__class__.model_fields.items():
17
17
  field_value = getattr(instance, field_name)
18
- if field_value == model_field.default:
19
- logger.warning(
20
- f"Using default value for '{field_name}': {model_field.default}"
21
- )
18
+ default_value = model_field.default
19
+
20
+ # Handle SecretStr comparison by comparing the secret values
21
+ if isinstance(field_value, SecretStr) and isinstance(default_value, SecretStr):
22
+ if field_value.get_secret_value() == default_value.get_secret_value():
23
+ logger.warning(
24
+ f"Using default value for '{field_name}': {default_value.get_secret_value()}"
25
+ )
26
+ elif field_value == default_value:
27
+ logger.warning(f"Using default value for '{field_name}': {default_value}")
22
28
  return instance
23
29
 
24
30
 
25
31
  class UniqueApp(BaseSettings):
26
- id: SecretStr = Field(default=SecretStr("dummy_id"))
27
- key: SecretStr = Field(default=SecretStr("dummy_key"))
32
+ id: SecretStr = Field(
33
+ default=SecretStr("dummy_id"),
34
+ validation_alias=AliasChoices(
35
+ "unique_app_id", "app_id", "UNIQUE_APP_ID", "APP_ID"
36
+ ),
37
+ )
38
+ key: SecretStr = Field(
39
+ default=SecretStr("dummy_key"),
40
+ validation_alias=AliasChoices(
41
+ "unique_app_key", "key", "UNIQUE_APP_KEY", "KEY", "API_KEY", "api_key"
42
+ ),
43
+ )
28
44
  base_url: str = Field(
29
45
  default="http://localhost:8092/",
30
46
  deprecated="Use UniqueApi.base_url instead",
@@ -48,8 +64,16 @@ class UniqueApi(BaseSettings):
48
64
  base_url: str = Field(
49
65
  default="http://localhost:8092/",
50
66
  description="The base URL of the Unique API. Ask your admin to provide you with the correct URL.",
67
+ validation_alias=AliasChoices(
68
+ "unique_api_base_url", "base_url", "UNIQUE_API_BASE_URL", "BASE_URL"
69
+ ),
70
+ )
71
+ version: str = Field(
72
+ default="2023-12-06",
73
+ validation_alias=AliasChoices(
74
+ "unique_api_version", "version", "UNIQUE_API_VERSION", "VERSION"
75
+ ),
51
76
  )
52
- version: str = Field(default="2023-12-06")
53
77
 
54
78
  model_config = SettingsConfigDict(
55
79
  env_prefix="unique_api_",
@@ -82,14 +106,29 @@ class UniqueApi(BaseSettings):
82
106
 
83
107
  def openai_proxy_url(self) -> str:
84
108
  parsed = urlparse(self.base_url)
85
- return urlunparse(
86
- parsed._replace(path="/public/openai-proxy/", query=None, fragment=None)
87
- )
109
+ path = "/public/chat/openai-proxy/"
110
+ if parsed.hostname and "qa.unique" in parsed.hostname:
111
+ path = "/public/chat-gen2/openai-proxy/"
112
+
113
+ return urlunparse(parsed._replace(path=path, query=None, fragment=None))
88
114
 
89
115
 
90
116
  class UniqueAuth(BaseSettings):
91
- company_id: SecretStr = Field(default=SecretStr("dummy_company_id"))
92
- user_id: SecretStr = Field(default=SecretStr("dummy_user_id"))
117
+ company_id: SecretStr = Field(
118
+ default=SecretStr("dummy_company_id"),
119
+ validation_alias=AliasChoices(
120
+ "unique_auth_company_id",
121
+ "company_id",
122
+ "UNIQUE_AUTH_COMPANY_ID",
123
+ "COMPANY_ID",
124
+ ),
125
+ )
126
+ user_id: SecretStr = Field(
127
+ default=SecretStr("dummy_user_id"),
128
+ validation_alias=AliasChoices(
129
+ "unique_auth_user_id", "user_id", "UNIQUE_AUTH_USER_ID", "USER_ID"
130
+ ),
131
+ )
93
132
 
94
133
  model_config = SettingsConfigDict(
95
134
  env_prefix="unique_auth_",
@@ -128,7 +167,7 @@ class UniqueSettings:
128
167
 
129
168
  # Initialize settings with environment file if provided
130
169
  env_file_str = str(env_file) if env_file else None
131
- auth = UniqueAuth(_env_file=env_file_str)
132
- app = UniqueApp(_env_file=env_file_str)
133
- api = UniqueApi(_env_file=env_file_str)
170
+ auth = UniqueAuth(_env_file=env_file_str) # type: ignore[call-arg]
171
+ app = UniqueApp(_env_file=env_file_str) # type: ignore[call-arg]
172
+ api = UniqueApi(_env_file=env_file_str) # type: ignore[call-arg]
134
173
  return cls(auth=auth, app=app, api=api)
@@ -10,9 +10,9 @@ def unique_history_to_langchain_history(
10
10
  history = []
11
11
  for m in unique_history:
12
12
  if m.role == UniqueRole.ASSISTANT:
13
- history.append(AIMessage(content=m.content))
13
+ history.append(AIMessage(content=m.content or ""))
14
14
  elif m.role == UniqueRole.USER:
15
- history.append(HumanMessage(content=m.content))
15
+ history.append(HumanMessage(content=m.content or ""))
16
16
  else:
17
17
  raise Exception("Unknown message role.")
18
18
 
@@ -37,7 +37,7 @@ def get_openai_client(unique_settings: UniqueSettings) -> OpenAI:
37
37
  default_headers = get_default_headers(unique_settings.app, unique_settings.auth)
38
38
 
39
39
  return OpenAI(
40
- api_key=unique_settings.app.key.get_secret_value(),
40
+ api_key="dummy_key",
41
41
  base_url=unique_settings.api.openai_proxy_url(),
42
42
  default_headers=default_headers,
43
43
  )
@@ -86,6 +86,24 @@ class LanguageModelFunction(BaseModel):
86
86
  return seralization
87
87
 
88
88
 
89
+ def __eq__(self, other:Self) -> bool:
90
+ """
91
+ Compare two tool calls based on name and arguments.
92
+ """
93
+ if not isinstance(other, LanguageModelFunction):
94
+ return False
95
+
96
+ if self.id != other.id:
97
+ return False
98
+
99
+ if self.name != other.name:
100
+ return False
101
+
102
+ if self.arguments != other.arguments:
103
+ return False
104
+
105
+ return True
106
+
89
107
  # This is tailored to the unique backend
90
108
  class LanguageModelStreamResponse(BaseModel):
91
109
  model_config = model_config
@@ -0,0 +1,72 @@
1
+ from unique_toolkit.content.schemas import ContentChunk, ContentReference
2
+ from unique_toolkit.tools.schemas import ToolCallResponse
3
+
4
+
5
+ class tool_chunks:
6
+ def __init__(self, name: str, chunks: list) -> None:
7
+ self.name = name
8
+ self.chunks = chunks
9
+
10
+
11
+ class ReferenceManager:
12
+ def __init__(self):
13
+ self._tool_chunks: dict[str, tool_chunks] = {}
14
+ self._chunks: list[ContentChunk] = []
15
+ self._references: list[list[ContentReference]] = []
16
+
17
+ def extract_referenceable_chunks(
18
+ self, tool_responses: list[ToolCallResponse]
19
+ ) -> None:
20
+ for tool_response in tool_responses:
21
+ if not tool_response.content_chunks:
22
+ continue
23
+ self._chunks.extend(tool_response.content_chunks or [])
24
+ self._tool_chunks[tool_response.id] = tool_chunks(
25
+ tool_response.name, tool_response.content_chunks
26
+ )
27
+
28
+ def get_chunks(self) -> list[ContentChunk]:
29
+ return self._chunks
30
+
31
+ def get_tool_chunks(self) -> dict:
32
+ return self._tool_chunks
33
+
34
+ def replace(self, chunks: list[ContentChunk]):
35
+ self._chunks = chunks
36
+
37
+ def add_references(
38
+ self,
39
+ references: list[ContentReference],
40
+ ):
41
+ self._references.append(references)
42
+
43
+ def get_references(
44
+ self,
45
+ ) -> list[list[ContentReference]]:
46
+ return self._references
47
+
48
+ def get_latest_references(
49
+ self,
50
+ ) -> list[ContentReference]:
51
+ if not self._references:
52
+ return []
53
+ return self._references[-1]
54
+
55
+ def get_latest_referenced_chunks(self) -> list[ContentChunk]:
56
+ if not self._references:
57
+ return []
58
+ return self._get_referenced_chunks_from_references(self._references[-1])
59
+
60
+ def _get_referenced_chunks_from_references(
61
+ self,
62
+ references: list[ContentReference],
63
+ ) -> list[ContentChunk]:
64
+ """
65
+ Get _referenced_chunks by matching sourceId from _references with merged id and chunk_id from _chunks.
66
+ """
67
+ referenced_chunks: list[ContentChunk] = []
68
+ for ref in references:
69
+ for chunk in self._chunks:
70
+ if ref.source_id == f"{chunk.id}-{chunk.chunk_id}":
71
+ referenced_chunks.append(chunk)
72
+ return referenced_chunks
@@ -0,0 +1,62 @@
1
+ from unique_toolkit.content.schemas import ContentChunk, ContentReference
2
+
3
+
4
+ class AgentChunksHandler:
5
+ def __init__(self):
6
+ self._tool_chunks = {}
7
+ self._chunks: list[ContentChunk] = []
8
+ self._references: list[list[ContentReference]] = []
9
+
10
+ @property
11
+ def chunks(self) -> list[ContentChunk]:
12
+ return self._chunks
13
+
14
+ @property
15
+ def tool_chunks(self) -> dict:
16
+ return self._tool_chunks
17
+
18
+ def extend(self, chunks: list[ContentChunk]):
19
+ self._chunks.extend(chunks)
20
+
21
+ def replace(self, chunks: list[ContentChunk]):
22
+ self._chunks = chunks
23
+
24
+ def add_references(
25
+ self,
26
+ references: list[ContentReference],
27
+ ):
28
+ self._references.append(references)
29
+
30
+ @property
31
+ def all_references(
32
+ self,
33
+ ) -> list[list[ContentReference]]:
34
+ return self._references
35
+
36
+ @property
37
+ def latest_references(
38
+ self,
39
+ ) -> list[ContentReference]:
40
+ if not self._references:
41
+ return []
42
+ return self._references[-1]
43
+
44
+ @property
45
+ def latest_referenced_chunks(self) -> list[ContentChunk]:
46
+ if not self._references:
47
+ return []
48
+ return self._get_referenced_chunks_from_references(self._references[-1])
49
+
50
+ def _get_referenced_chunks_from_references(
51
+ self,
52
+ references: list[ContentReference],
53
+ ) -> list[ContentChunk]:
54
+ """
55
+ Get _referenced_chunks by matching sourceId from _references with merged id and chunk_id from _chunks.
56
+ """
57
+ referenced_chunks: list[ContentChunk] = []
58
+ for ref in references:
59
+ for chunk in self._chunks:
60
+ if ref.source_id == str(chunk.id) + "_" + str(chunk.chunk_id):
61
+ referenced_chunks.append(chunk)
62
+ return referenced_chunks
@@ -0,0 +1,108 @@
1
+ from enum import StrEnum
2
+ import humps
3
+ from typing import Any
4
+ from pydantic.fields import ComputedFieldInfo, FieldInfo
5
+ from pydantic.alias_generators import to_camel
6
+ from pydantic import (
7
+ BaseModel,
8
+ ConfigDict,
9
+ Field,
10
+ ValidationInfo,
11
+ model_validator,
12
+ )
13
+
14
+ from typing import TYPE_CHECKING
15
+
16
+ if TYPE_CHECKING:
17
+ from unique_toolkit.tools.schemas import BaseToolConfig
18
+
19
+
20
+ def field_title_generator(
21
+ title: str,
22
+ info: FieldInfo | ComputedFieldInfo,
23
+ ) -> str:
24
+ return humps.decamelize(title).replace("_", " ").title()
25
+
26
+
27
+ def model_title_generator(model: type) -> str:
28
+ return humps.decamelize(model.__name__).replace("_", " ").title()
29
+
30
+
31
+ def get_configuration_dict(**kwargs) -> ConfigDict:
32
+ return ConfigDict(
33
+ alias_generator=to_camel,
34
+ field_title_generator=field_title_generator,
35
+ model_title_generator=model_title_generator,
36
+ populate_by_name=True,
37
+ protected_namespaces=(),
38
+ **kwargs,
39
+ )
40
+
41
+
42
+ class ToolIcon(StrEnum):
43
+ ANALYTICS = "IconAnalytics"
44
+ BOOK = "IconBook"
45
+ FOLDERDATA = "IconFolderData"
46
+ INTEGRATION = "IconIntegration"
47
+ TEXT_COMPARE = "IconTextCompare"
48
+ WORLD = "IconWorld"
49
+ QUICK_REPLY = "IconQuickReply"
50
+ CHAT_PLUS = "IconChatPlus"
51
+
52
+
53
+ class ToolSelectionPolicy(StrEnum):
54
+ """Determine the usage policy of tools."""
55
+
56
+ FORCED_BY_DEFAULT = "ForcedByDefault"
57
+ ON_BY_DEFAULT = "OnByDefault"
58
+ BY_USER = "ByUser"
59
+
60
+
61
+ class ToolBuildConfig(BaseModel):
62
+ model_config = get_configuration_dict()
63
+ """Main tool configuration"""
64
+
65
+ name: str
66
+ configuration: "BaseToolConfig"
67
+ display_name: str = ""
68
+ icon: ToolIcon = ToolIcon.BOOK
69
+ selection_policy: ToolSelectionPolicy = Field(
70
+ default=ToolSelectionPolicy.BY_USER,
71
+ )
72
+ is_exclusive: bool = Field(
73
+ default=False,
74
+ description="This tool must be chosen by the user and no other tools are used for this iteration.",
75
+ )
76
+
77
+ is_enabled: bool = Field(default=True)
78
+
79
+ @model_validator(mode="before")
80
+ def initialize_config_based_on_tool_name(
81
+ cls,
82
+ value: Any,
83
+ info: ValidationInfo,
84
+ ) -> Any:
85
+ """Check the given values for."""
86
+ if not isinstance(value, dict):
87
+ return value
88
+
89
+ configuration = value.get("configuration", {})
90
+ if isinstance(configuration, dict):
91
+ # Local import to avoid circular import at module import time
92
+ from unique_toolkit.tools.factory import ToolFactory
93
+
94
+ config = ToolFactory.build_tool_config(
95
+ value["name"],
96
+ **configuration,
97
+ )
98
+ else:
99
+ # Check that the type of config matches the tool name
100
+ from unique_toolkit.tools.factory import ToolFactory
101
+
102
+ assert isinstance(
103
+ configuration,
104
+ ToolFactory.tool_config_map[value["name"]], # type: ignore
105
+ )
106
+ config = configuration
107
+ value["configuration"] = config
108
+ return value
@@ -1,7 +1,11 @@
1
1
  from typing import Callable
2
2
 
3
- from unique_toolkit.unique_toolkit.tools.tool_definitions import BaseToolConfig, Tool
3
+ from typing import TYPE_CHECKING
4
+ from unique_toolkit.tools.schemas import BaseToolConfig
5
+ from unique_toolkit.tools.tool import Tool
4
6
 
7
+ if TYPE_CHECKING:
8
+ from unique_toolkit.tools.config import ToolBuildConfig
5
9
 
6
10
 
7
11
  class ToolFactory:
@@ -18,14 +22,20 @@ class ToolFactory:
18
22
  cls.tool_config_map[tool.name] = tool_config
19
23
 
20
24
  @classmethod
21
- def build_tool(cls, tool_name: str, *args, **kwargs) -> Tool:
25
+ def build_tool(cls, tool_name: str, *args, **kwargs) -> Tool[BaseToolConfig]:
22
26
  tool = cls.tool_map[tool_name](*args, **kwargs)
23
27
  return tool
24
28
 
25
29
  @classmethod
26
- def build_tool_config(
27
- cls, tool_name: str, **kwargs
28
- ) -> BaseToolConfig:
30
+ def build_tool_with_settings(
31
+ cls, tool_name: str, settings: "ToolBuildConfig", *args, **kwargs
32
+ ) -> Tool[BaseToolConfig]:
33
+ tool = cls.tool_map[tool_name](*args, **kwargs)
34
+ tool.settings = settings
35
+ return tool
36
+
37
+ @classmethod
38
+ def build_tool_config(cls, tool_name: str, **kwargs) -> BaseToolConfig:
29
39
  if tool_name not in cls.tool_config_map:
30
40
  raise ValueError(f"Tool {tool_name} not found")
31
41
  return cls.tool_config_map[tool_name](**kwargs)
@@ -0,0 +1,138 @@
1
+ import base64
2
+ import gzip
3
+ import re
4
+ from typing import Any, Optional
5
+
6
+ from pydantic import BaseModel, ConfigDict, Field, field_serializer, field_validator
7
+ from unique_toolkit.content.schemas import ContentChunk
8
+
9
+ from unique_toolkit.tools.config import get_configuration_dict
10
+ from unique_toolkit.tools.utils.source_handling.schema import SourceFormatConfig
11
+
12
+
13
+ # TODO: this needs to be more general as the tools can potentially return anything maybe make a base class and then derive per "type" of tool
14
+ class ToolCallResponse(BaseModel):
15
+ id: str
16
+ name: str
17
+ debug_info: Optional[dict] = None # TODO: Make the default {}
18
+ content_chunks: Optional[list[ContentChunk]] = None # TODO: Make the default []
19
+ reasoning_result: Optional[dict] = None # TODO: Make the default {}
20
+ error_message: str = ""
21
+
22
+ @property
23
+ def successful(self) -> bool:
24
+ return self.error_message == ""
25
+
26
+
27
+ class BaseToolConfig(BaseModel):
28
+ model_config = get_configuration_dict()
29
+ # TODO: add a check for the parameters to all be consistent within the tool config
30
+ pass
31
+
32
+
33
+ class Source(BaseModel):
34
+ """Represents the sources in the tool call response that the llm will see
35
+
36
+ Args:
37
+ source_number: The number of the source
38
+ content: The content of the source
39
+ """
40
+
41
+ model_config = ConfigDict(
42
+ validate_by_alias=True, serialize_by_alias=True, validate_by_name=True
43
+ )
44
+
45
+ source_number: int | None = Field(
46
+ default=None,
47
+ serialization_alias="[source_number] - Used for citations!",
48
+ validation_alias="[source_number] - Used for citations!",
49
+ )
50
+ content: str = Field(
51
+ serialization_alias="[content] - Content of source",
52
+ validation_alias="[content] - Content of source",
53
+ )
54
+ order: int = Field(
55
+ serialization_alias="[order] - Index in the document!",
56
+ validation_alias="[order] - Index in the document!",
57
+ )
58
+ chunk_id: str | None = Field(
59
+ default=None,
60
+ serialization_alias="[chunk_id] - IGNORE",
61
+ validation_alias="[chunk_id] - IGNORE",
62
+ )
63
+ id: str = Field(
64
+ serialization_alias="[id] - IGNORE",
65
+ validation_alias="[id] - IGNORE",
66
+ )
67
+ key: str | None = Field(
68
+ default=None,
69
+ serialization_alias="[key] - IGNORE",
70
+ validation_alias="[key] - IGNORE",
71
+ )
72
+ metadata: dict[str, str] | str | None = Field(
73
+ default=None,
74
+ serialization_alias="[metadata] - Formatted metadata",
75
+ validation_alias="[metadata] - Formatted metadata",
76
+ )
77
+ url: str | None = Field(
78
+ default=None,
79
+ serialization_alias="[url] - IGNORE",
80
+ validation_alias="[url] - IGNORE",
81
+ )
82
+
83
+ @field_validator("metadata", mode="before")
84
+ def _metadata_str_to_dict(
85
+ cls, v: str | dict[str, str] | None
86
+ ) -> dict[str, str] | None:
87
+ """
88
+ Accept • dict → keep as-is
89
+ • str → parse tag-string back to dict
90
+ """
91
+ if v is None or isinstance(v, dict):
92
+ return v
93
+
94
+ # v is the rendered string. Build a dict by matching the
95
+ # patterns defined in SourceFormatConfig.sections.
96
+ cfg = SourceFormatConfig() # or inject your app-wide config
97
+ out: dict[str, str] = {}
98
+ for key, tmpl in cfg.sections.items():
99
+ pattern = cfg.template_to_pattern(tmpl)
100
+ m = re.search(pattern, v, flags=re.S)
101
+ if m:
102
+ out[key] = m.group(1).strip()
103
+
104
+ return out if out else v # type: ignore
105
+
106
+ # Compression + Base64 for url to hide it from the LLM
107
+ @field_serializer("url")
108
+ def serialize_url(self, value: str | None) -> str | None:
109
+ if value is None:
110
+ return None
111
+ # Compress then base64 encode
112
+ compressed = gzip.compress(value.encode())
113
+ return base64.b64encode(compressed).decode()
114
+
115
+ @field_validator("url", mode="before")
116
+ @classmethod
117
+ def validate_url(cls, value: Any) -> str | None:
118
+ if value is None or isinstance(value, str) and not value:
119
+ return None
120
+ if isinstance(value, str):
121
+ try:
122
+ # Try to decode base64 then decompress
123
+ decoded_bytes = base64.b64decode(value.encode())
124
+ decompressed = gzip.decompress(decoded_bytes).decode()
125
+ return decompressed
126
+ except Exception:
127
+ # If decoding/decompression fails, assume it's plain text
128
+ return value
129
+ return str(value)
130
+
131
+
132
+ class ToolPrompts(BaseModel):
133
+ name: str
134
+ display_name: str
135
+ tool_description: str
136
+ tool_format_information_for_system_prompt: str
137
+ tool_format_information_for_system_prompt: str
138
+ input_model: dict[str, Any]