unique_toolkit 0.7.41__tar.gz → 0.8.0__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 (81) hide show
  1. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/CHANGELOG.md +6 -0
  2. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/PKG-INFO +8 -2
  3. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/pyproject.toml +3 -3
  4. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/app/__init__.py +6 -0
  5. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/app/schemas.py +51 -0
  6. unique_toolkit-0.8.0/unique_toolkit/tools/tool_definitions.py +145 -0
  7. unique_toolkit-0.8.0/unique_toolkit/tools/tool_definitionsV2.py +137 -0
  8. unique_toolkit-0.8.0/unique_toolkit/tools/tool_factory.py +31 -0
  9. unique_toolkit-0.8.0/unique_toolkit/tools/tool_progress_reporter.py +225 -0
  10. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/LICENSE +0 -0
  11. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/README.md +0 -0
  12. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/__init__.py +0 -0
  13. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/_common/_base_service.py +0 -0
  14. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/_common/_time_utils.py +0 -0
  15. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/_common/exception.py +0 -0
  16. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/_common/validate_required_values.py +0 -0
  17. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/_common/validators.py +0 -0
  18. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/app/event_util.py +0 -0
  19. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/app/init_logging.py +0 -0
  20. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/app/init_sdk.py +0 -0
  21. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/app/performance/async_tasks.py +0 -0
  22. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/app/performance/async_wrapper.py +0 -0
  23. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/app/sse_client.py +0 -0
  24. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/app/unique_settings.py +0 -0
  25. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/app/verification.py +0 -0
  26. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/chat/__init__.py +0 -0
  27. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/chat/constants.py +0 -0
  28. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/chat/functions.py +0 -0
  29. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/chat/schemas.py +0 -0
  30. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/chat/service.py +0 -0
  31. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/chat/state.py +0 -0
  32. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/chat/utils.py +0 -0
  33. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/content/__init__.py +0 -0
  34. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/content/constants.py +0 -0
  35. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/content/functions.py +0 -0
  36. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/content/schemas.py +0 -0
  37. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/content/service.py +0 -0
  38. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/content/utils.py +0 -0
  39. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/embedding/__init__.py +0 -0
  40. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/embedding/constants.py +0 -0
  41. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/embedding/functions.py +0 -0
  42. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/embedding/schemas.py +0 -0
  43. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/embedding/service.py +0 -0
  44. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/embedding/utils.py +0 -0
  45. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/evaluators/__init__.py +0 -0
  46. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/evaluators/config.py +0 -0
  47. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/evaluators/constants.py +0 -0
  48. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/evaluators/context_relevancy/constants.py +0 -0
  49. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/evaluators/context_relevancy/prompts.py +0 -0
  50. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/evaluators/context_relevancy/service.py +0 -0
  51. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/evaluators/context_relevancy/utils.py +0 -0
  52. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/evaluators/exception.py +0 -0
  53. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/evaluators/hallucination/constants.py +0 -0
  54. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/evaluators/hallucination/prompts.py +0 -0
  55. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/evaluators/hallucination/service.py +0 -0
  56. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/evaluators/hallucination/utils.py +0 -0
  57. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/evaluators/output_parser.py +0 -0
  58. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/evaluators/schemas.py +0 -0
  59. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/framework_utilities/langchain/client.py +0 -0
  60. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/framework_utilities/langchain/history.py +0 -0
  61. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/framework_utilities/openai/client.py +0 -0
  62. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/framework_utilities/openai/message_builder.py +0 -0
  63. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/framework_utilities/utils.py +0 -0
  64. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/language_model/__init__.py +0 -0
  65. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/language_model/builder.py +0 -0
  66. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/language_model/constants.py +0 -0
  67. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/language_model/functions.py +0 -0
  68. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/language_model/infos.py +0 -0
  69. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/language_model/prompt.py +0 -0
  70. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/language_model/reference.py +0 -0
  71. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/language_model/schemas.py +0 -0
  72. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/language_model/service.py +0 -0
  73. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/language_model/utils.py +0 -0
  74. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/protocols/support.py +0 -0
  75. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/short_term_memory/__init__.py +0 -0
  76. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/short_term_memory/constants.py +0 -0
  77. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/short_term_memory/functions.py +0 -0
  78. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/short_term_memory/schemas.py +0 -0
  79. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/short_term_memory/service.py +0 -0
  80. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/unique_toolkit/smart_rules/__init__.py +0 -0
  81. {unique_toolkit-0.7.41 → unique_toolkit-0.8.0}/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.0] - 2025-08-04
9
+ - Add MCP support
10
+
11
+ ## [0.7.42] - 2025-08-01
12
+ - Added tool definitions
13
+
8
14
  ## [0.7.41] - 2025-07-31
9
15
  - Add new chat event attribute indicating tools disabled on a company level
10
16
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: unique_toolkit
3
- Version: 0.7.41
3
+ Version: 0.8.0
4
4
  Summary:
5
5
  License: Proprietary
6
6
  Author: Martin Fadler
@@ -19,7 +19,7 @@ Requires-Dist: regex (>=2024.5.15,<2025.0.0)
19
19
  Requires-Dist: sseclient (>=0.0.27,<0.0.28)
20
20
  Requires-Dist: tiktoken (>=0.7.0,<0.8.0)
21
21
  Requires-Dist: typing-extensions (>=4.9.0,<5.0.0)
22
- Requires-Dist: unique-sdk (>=0.9.40,<0.10.0)
22
+ Requires-Dist: unique-sdk (>=0.10.0,<0.11.0)
23
23
  Description-Content-Type: text/markdown
24
24
 
25
25
  # Unique Toolkit
@@ -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.0] - 2025-08-04
117
+ - Add MCP support
118
+
119
+ ## [0.7.42] - 2025-08-01
120
+ - Added tool definitions
121
+
116
122
  ## [0.7.41] - 2025-07-31
117
123
  - Add new chat event attribute indicating tools disabled on a company level
118
124
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "unique_toolkit"
3
- version = "0.7.41"
3
+ version = "0.8.0"
4
4
  description = ""
5
5
  authors = [
6
6
  "Martin Fadler <martin.fadler@unique.ch>",
@@ -20,7 +20,7 @@ numpy = "^1.26.4"
20
20
  python-dotenv = "^1.0.1"
21
21
  regex = "^2024.5.15"
22
22
  tiktoken = "^0.7.0"
23
- unique-sdk = "^0.9.40"
23
+ unique-sdk = "^0.10.0"
24
24
  pydantic-settings = "^2.10.1"
25
25
  sseclient = "^0.0.27"
26
26
 
@@ -47,7 +47,7 @@ pytest-cov = "^4.1.0"
47
47
  pre-commit = "^3.7.1"
48
48
  pytest-asyncio = "^0.23.8"
49
49
  pytest-mock = "^3.14.0"
50
- unique-sdk = {path = "../unique_sdk"}
50
+ unique-sdk = { path = "../unique_sdk" }
51
51
  ipykernel = "^6.29.5"
52
52
 
53
53
  [build-system]
@@ -41,5 +41,11 @@ from .schemas import (
41
41
  from .verification import (
42
42
  verify_signature_and_construct_event as verify_signature_and_construct_event,
43
43
  )
44
+ from .schemas import (
45
+ McpServer as McpServer,
46
+ )
47
+ from .schemas import (
48
+ McpTool as McpTool,
49
+ )
44
50
 
45
51
  DOMAIN_NAME = "app"
@@ -38,6 +38,53 @@ class BaseEvent(BaseModel):
38
38
  return cls.model_validate(data)
39
39
 
40
40
 
41
+ ###
42
+ # MCP schemas
43
+ ###
44
+
45
+ class McpTool(BaseModel):
46
+ model_config = model_config
47
+
48
+ name: str
49
+ description: Optional[str] = None
50
+ input_schema: dict[str, Any]
51
+ output_schema: Optional[dict[str, Any]] = None
52
+ annotations: Optional[dict[str, Any]] = None
53
+ title: Optional[str] = Field(
54
+ default=None,
55
+ description="The display title for a tool. This is a Unique specific field.",
56
+ )
57
+ icon: Optional[str] = Field(
58
+ default=None,
59
+ description="An icon name from the Lucide icon set for the tool. This is a Unique specific field.",
60
+ )
61
+ system_prompt: Optional[str] = Field(
62
+ default=None,
63
+ description="An optional system prompt for the tool. This is a Unique specific field.",
64
+ )
65
+ user_prompt: Optional[str] = Field(
66
+ default=None,
67
+ description="An optional user prompt for the tool. This is a Unique specific field.",
68
+ )
69
+ is_connected: bool = Field(
70
+ description="Whether the tool is connected to the MCP server. This is a Unique specific field.",
71
+ )
72
+
73
+ class McpServer(BaseModel):
74
+ model_config = model_config
75
+
76
+ id: str
77
+ name: str
78
+ system_prompt: Optional[str] = Field(
79
+ default=None,
80
+ description="An optional system prompt for the MCP server.",
81
+ )
82
+ user_prompt: Optional[str] = Field(
83
+ default=None,
84
+ description="An optional user prompt for the MCP server.",
85
+ )
86
+ tools: list[McpTool] = []
87
+
41
88
  ###
42
89
  # ChatEvent schemas
43
90
  ###
@@ -133,6 +180,10 @@ class ChatEventPayload(BaseModel):
133
180
  default=None,
134
181
  description="Raw UniqueQL rule that can be compiled to a metadata filter.",
135
182
  )
183
+ mcp_servers: list[McpServer] = Field(
184
+ default_factory=list,
185
+ description="A list of MCP servers with tools available for the chat session.",
186
+ )
136
187
 
137
188
  @field_validator("raw_scope_rules", mode="before")
138
189
  def validate_scope_rules(cls, value: dict[str, Any] | None) -> UniqueQL | None:
@@ -0,0 +1,145 @@
1
+ from abc import ABC, abstractmethod
2
+ from enum import StrEnum
3
+ from typing import Any, Generic, List, Self, TypeVar
4
+ from unique_toolkit.language_model import LanguageModelToolDescription
5
+ from typing_extensions import deprecated
6
+ # import baseModel from pedantic
7
+ from unique_toolkit.language_model import LanguageModelFunction
8
+ from pydantic import BaseModel, Field, model_validator, root_validator
9
+
10
+ from unique_toolkit.unique_toolkit.app.schemas import ChatEvent
11
+ from unique_toolkit.unique_toolkit.tools.tool_progress_reporter import ToolProgressReporter
12
+
13
+ class ToolSelectionPolicy(StrEnum):
14
+ """Determine the usage policy of tools."""
15
+
16
+ FORCED_BY_DEFAULT = "ForcedByDefault"
17
+ ON_BY_DEFAULT = "OnByDefault"
18
+ BY_USER = "ByUser"
19
+
20
+ class UqToolName(StrEnum):
21
+ WEB_SEARCH = "WebSearch"
22
+ INTERNAL_SEARCH = "InternalSearch"
23
+ DOCUMENT_SUMMARIZER = "DocumentSummarizer"
24
+ CHART_GENERATOR = "ChartGenerator"
25
+ DOCUMENT_GENERATOR = "DocumentGenerator"
26
+ DOCUMENT_PARSER = "DocumentParser"
27
+ IMAGE_CONTENT = "ImageContent"
28
+ TABLE_SEARCH = "TableSearch"
29
+ BAR_CHART = "BarChart"
30
+ LINE_CHART = "LineChart"
31
+ PIE_CHART = "PieChart"
32
+ BASE_TOOL = "BaseTool"
33
+
34
+
35
+ class BaseToolConfig(BaseModel):
36
+ pass
37
+
38
+
39
+ ConfigType = TypeVar("ConfigType", bound=BaseToolConfig)
40
+
41
+
42
+ class ToolSettings(Generic[ConfigType]):
43
+ configuration: ConfigType
44
+ display_name: str
45
+ icon: str
46
+ selection_policy: ToolSelectionPolicy = Field(
47
+ default=ToolSelectionPolicy.BY_USER,
48
+ )
49
+ is_exclusive: bool = Field(default=False)
50
+ is_enabled: bool = Field(default=True)
51
+
52
+ @classmethod
53
+ def from_service_dict(cls, service_dict: dict[str, Any]) -> Self | None:
54
+ try:
55
+ return cls(**service_dict)
56
+ except (ValueError, TypeError) as e:
57
+ print(e)
58
+ return None
59
+
60
+
61
+ class ToolCallResponse(BaseModel):
62
+ id: str
63
+ name: str
64
+ debug_info: dict = {}
65
+
66
+
67
+ class ToolPromptInstructions(BaseModel):
68
+ system_prompt: str = Field(
69
+ default="",
70
+ description=("Helps the LLM understand how to use the tool. "
71
+ "This is injected into the system prompt."
72
+ "This might not be needed for every tool but some of the work better with user prompt "
73
+ "instructions while others work better with system prompt instructions."),
74
+ )
75
+
76
+ user_prompt: str = Field(
77
+ default="",
78
+ description=("Helps the LLM understand how to use the tool. "
79
+ "This is injected into the user prompt. "
80
+ "This might not be needed for every tool but some of the work better with user prompt "
81
+ "instructions while others work better with system prompt instructions.")
82
+ )
83
+
84
+ system_prompt_tool_chosen: str = Field(
85
+ default="",
86
+ description=("Once the tool is chosen, this is injected into the system prompt"
87
+ " to help the LLM understand how work with the tools results."),
88
+ )
89
+
90
+ user_prompt_tool_chosen: str = Field(
91
+ default="",
92
+ description=("Once the tool is chosen, this is injected into the user prompt "
93
+ "to help the LLM understand how to work with the tools results."),
94
+ )
95
+
96
+
97
+ class Tool(ABC, Generic[ConfigType]):
98
+ name: str
99
+
100
+ def tool_description(self) -> LanguageModelToolDescription:
101
+ raise NotImplementedError
102
+
103
+
104
+ def get_prompt_instructions(self) -> ToolPromptInstructions:
105
+ return ToolPromptInstructions(
106
+ system_prompt="",
107
+ user_prompt="",
108
+ system_prompt_tool_chosen="",
109
+ user_prompt_tool_chosen="",
110
+ )
111
+
112
+
113
+ def is_exclusive(self) -> bool:
114
+ return self.settings.is_exclusive
115
+
116
+ def is_enabled(self) -> bool:
117
+ return self.settings.is_enabled
118
+
119
+ def display_name(self) -> str:
120
+ return self.settings.display_name
121
+
122
+ def icon(self) -> str:
123
+ return self.settings.icon
124
+
125
+ def tool_selection_policy(self) -> ToolSelectionPolicy:
126
+ return self.settings.selection_policy
127
+
128
+
129
+ @abstractmethod
130
+ async def run(self, tool_call: LanguageModelFunction) -> ToolCallResponse:
131
+ raise NotImplementedError
132
+
133
+
134
+
135
+ def __init__(
136
+ self,
137
+ settings: ToolSettings[ConfigType],
138
+ event: ChatEvent,
139
+ tool_progress_reporter: ToolProgressReporter
140
+ ):
141
+ self.settings = settings
142
+ self.tool_progress_reporter = tool_progress_reporter
143
+ self.event = event
144
+
145
+
@@ -0,0 +1,137 @@
1
+ from abc import ABC, abstractmethod
2
+ from enum import StrEnum
3
+ from typing import Any, Generic, List, Self, TypeVar
4
+ from unique_toolkit.language_model import LanguageModelToolDescription
5
+ from typing_extensions import deprecated
6
+ # import baseModel from pedantic
7
+ from unique_toolkit.language_model import LanguageModelFunction
8
+ from pydantic import BaseModel, Field, model_validator, root_validator
9
+
10
+ from unique_toolkit.unique_toolkit.app.schemas import ChatEvent
11
+ from unique_toolkit.unique_toolkit.tools.tool_definitions import BaseToolConfig, Tool, ToolCallResponse, ToolPromptInstructions, ToolSettings
12
+ from unique_toolkit.unique_toolkit.tools.tool_progress_reporter import ToolProgressReporter
13
+
14
+
15
+ class BaseToolConfigV2(BaseToolConfig):
16
+ class ToolCallConfig(BaseModel):
17
+ description: str = Field(
18
+ default="Base",
19
+ description="The tool description must be set by subclasses",
20
+ )
21
+
22
+ parameters: type[BaseModel] = Field(
23
+ default=BaseModel,
24
+ description="The tool parameters configuration must be set by subclasses",
25
+ )
26
+
27
+ class PromptInstructionsConfig(BaseModel):
28
+ system_prompt: str = Field(
29
+ default="",
30
+ description=("Helps the LLM understand how to use the tool. "
31
+ "This is injected into the system prompt."
32
+ "This might not be needed for every tool but some of the work better with user prompt "
33
+ "instructions while others work better with system prompt instructions."),
34
+ )
35
+
36
+ user_prompt: str = Field(
37
+ default="",
38
+ description=("Helps the LLM understand how to use the tool. "
39
+ "This is injected into the user prompt. "
40
+ "This might not be needed for every tool but some of the work better with user prompt "
41
+ "instructions while others work better with system prompt instructions.")
42
+ )
43
+
44
+ system_prompt_tool_chosen: str = Field(
45
+ default="",
46
+ description=("Once the tool is chosen, this is injected into the system prompt"
47
+ " to help the LLM understand how work with the tools results."),
48
+ )
49
+
50
+ user_prompt_tool_chosen: str = Field(
51
+ default="",
52
+ description=("Once the tool is chosen, this is injected into the user prompt "
53
+ "to help the LLM understand how to work with the tools results."),
54
+ )
55
+
56
+ tool_call: ToolCallConfig = Field(
57
+ default_factory=ToolCallConfig,
58
+ description="Configuration for the tool, including description and parameters",
59
+ )
60
+
61
+ prompts: PromptInstructionsConfig = Field(
62
+ default_factory=PromptInstructionsConfig,
63
+ description="Configuration for prompts related to the tool",
64
+ )
65
+
66
+ # This makes sure that the settings are all present in all subclasses and that they define a default value.
67
+ @model_validator(mode="after")
68
+ def validate_tool_description(cls):
69
+ if cls.__class__ is BaseToolConfig:
70
+ return cls # Skip validation for the base class
71
+ if cls.tool_call.description == "Base":
72
+ raise ValueError(
73
+ f"Subclass {cls.__class__.__name__} must define a default value for 'tool_description'."
74
+ )
75
+ if cls.tool_call.parameters == BaseModel:
76
+ raise ValueError(
77
+ f"Subclass {cls.__class__.__name__} must define a default value for 'tool_parameters_config'."
78
+ )
79
+ if cls.prompts.system_prompt == "":
80
+ raise ValueError(
81
+ f"Subclass {cls.__class__.__name__} must define a default value for 'system_prompt_base_instructions'."
82
+ )
83
+ if cls.prompts.user_prompt == "":
84
+ raise ValueError(
85
+ f"Subclass {cls.__class__.__name__} must define a default value for 'user_prompt_base_instructions'."
86
+ )
87
+ if cls.prompts.system_prompt_tool_chosen == "":
88
+ raise ValueError(
89
+ f"Subclass {cls.__class__.__name__} must define a default value for 'system_prompt_tool_chosen_instructions'."
90
+ )
91
+ if cls.prompts.user_prompt_tool_chosen == "":
92
+ raise ValueError(
93
+ f"Subclass {cls.__class__.__name__} must define a default value for 'user_prompt_tool_chosen_instructions'."
94
+ )
95
+ return cls
96
+
97
+
98
+ ConfigTypeV2 = TypeVar("ConfigTypeV2", bound=BaseToolConfigV2)
99
+
100
+
101
+
102
+ class ToolV2(Tool[ConfigTypeV2]):
103
+ name: str
104
+
105
+ def tool_description(self) -> LanguageModelToolDescription:
106
+ return LanguageModelToolDescription(
107
+ name=self.name,
108
+ description=self.settings.configuration.tool_call.description,
109
+ parameters=self.settings.configuration.tool_call.parameters,
110
+ )
111
+
112
+ def get_prompt_instructions(self) -> ToolPromptInstructions:
113
+ return ToolPromptInstructions(
114
+ system_prompt=self.settings.configuration.prompts.system_prompt,
115
+ user_prompt=self.settings.configuration.prompts.user_prompt,
116
+ system_prompt_tool_chosen=self.settings.configuration.prompts.system_prompt_tool_chosen,
117
+ user_prompt_tool_chosen=self.settings.configuration.prompts.user_prompt_tool_chosen
118
+ )
119
+
120
+
121
+ @abstractmethod
122
+ async def run(self, tool_call: LanguageModelFunction) -> ToolCallResponse:
123
+ raise NotImplementedError
124
+
125
+
126
+
127
+ def __init__(
128
+ self,
129
+ settings: ToolSettings[ConfigTypeV2],
130
+ event: ChatEvent,
131
+ tool_progress_reporter: ToolProgressReporter
132
+ ):
133
+ self.settings = settings
134
+ self.tool_progress_reporter = tool_progress_reporter
135
+ self.event = event
136
+
137
+
@@ -0,0 +1,31 @@
1
+ from typing import Callable
2
+
3
+ from unique_toolkit.unique_toolkit.tools.tool_definitions import BaseToolConfig, Tool
4
+
5
+
6
+
7
+ class ToolFactory:
8
+ tool_map: dict[str, type[Tool]] = {}
9
+ tool_config_map: dict[str, Callable] = {}
10
+
11
+ @classmethod
12
+ def register_tool(
13
+ cls,
14
+ tool: type[Tool],
15
+ tool_config: type[BaseToolConfig],
16
+ ):
17
+ cls.tool_map[tool.name] = tool
18
+ cls.tool_config_map[tool.name] = tool_config
19
+
20
+ @classmethod
21
+ def build_tool(cls, tool_name: str, *args, **kwargs) -> Tool:
22
+ tool = cls.tool_map[tool_name](*args, **kwargs)
23
+ return tool
24
+
25
+ @classmethod
26
+ def build_tool_config(
27
+ cls, tool_name: str, **kwargs
28
+ ) -> BaseToolConfig:
29
+ if tool_name not in cls.tool_config_map:
30
+ raise ValueError(f"Tool {tool_name} not found")
31
+ return cls.tool_config_map[tool_name](**kwargs)
@@ -0,0 +1,225 @@
1
+ import re
2
+ from datetime import datetime
3
+ from enum import Enum
4
+ from functools import wraps
5
+ from typing import Protocol
6
+
7
+ from pydantic import BaseModel
8
+ from unique_toolkit.chat.service import ChatService
9
+ from unique_toolkit.content.schemas import ContentReference
10
+ from unique_toolkit.language_model.schemas import (
11
+ LanguageModelFunction,
12
+ LanguageModelStreamResponse,
13
+ )
14
+
15
+ ARROW = "&#8594;&nbsp;"
16
+ DUMMY_REFERENCE_PLACEHOLDER = "<sup></sup>"
17
+
18
+
19
+ class ProgressState(Enum):
20
+ STARTED = "⚪"
21
+ RUNNING = "🟡"
22
+ FAILED = "🔴"
23
+ FINISHED = "🟢"
24
+
25
+
26
+ class ToolExecutionStatus(BaseModel):
27
+ name: str
28
+ message: str
29
+ state: ProgressState
30
+ references: list[ContentReference] = []
31
+ timestamp: datetime = datetime.now()
32
+
33
+
34
+ class ToolProgressReporter:
35
+ def __init__(self, chat_service: ChatService):
36
+ self.chat_service = chat_service
37
+ self.tool_statuses: dict[str, ToolExecutionStatus] = {}
38
+ self._progress_start_text = ""
39
+ self._requires_new_assistant_message = False
40
+
41
+ @property
42
+ def requires_new_assistant_message(self):
43
+ return self._requires_new_assistant_message
44
+
45
+ @requires_new_assistant_message.setter
46
+ def requires_new_assistant_message(self, value: bool):
47
+ self._requires_new_assistant_message = value
48
+
49
+ @property
50
+ def tool_statuses_is_empty(self):
51
+ return len(self.tool_statuses) == 0
52
+
53
+ def empty_tool_statuses_if_stream_has_text(
54
+ self, stream_response: LanguageModelStreamResponse
55
+ ):
56
+ if stream_response.message.text:
57
+ self.tool_statuses = {}
58
+
59
+ async def notify_from_tool_call(
60
+ self,
61
+ tool_call: LanguageModelFunction,
62
+ name: str,
63
+ message: str,
64
+ state: ProgressState,
65
+ references: list[ContentReference] = [],
66
+ requires_new_assistant_message: bool = False,
67
+ ):
68
+ """
69
+ Notifies about a tool call execution status and updates the assistant message.
70
+
71
+ Args:
72
+ tool_call (LanguageModelFunction): The tool call being executed
73
+ name (str): Name of the tool being executed
74
+ message (str): Status message to display
75
+ state (ProgressState): Current execution state of the tool
76
+ references (list[ContentReference], optional): List of content references. Defaults to [].
77
+ requires_new_assistant_message (bool, optional): Whether a new assistant message is needed when tool call is finished.
78
+ Defaults to False. If yes, the agentic steps will remain in chat history and will be overwritten by the stream response.
79
+
80
+ Raises:
81
+ AssertionError: If tool_call.id is None
82
+ """
83
+ assert tool_call.id is not None
84
+ self.tool_statuses[tool_call.id] = ToolExecutionStatus(
85
+ name=name,
86
+ message=message,
87
+ state=state,
88
+ references=references,
89
+ )
90
+ self.requires_new_assistant_message = (
91
+ self.requires_new_assistant_message
92
+ or requires_new_assistant_message
93
+ )
94
+ await self.publish()
95
+
96
+ async def publish(self):
97
+ messages = []
98
+ all_references = []
99
+ for item in sorted(
100
+ self.tool_statuses.values(), key=lambda x: x.timestamp
101
+ ):
102
+ references = item.references
103
+ start_number = len(all_references) + 1
104
+ message = self._replace_placeholders(item.message, start_number)
105
+ references = self._correct_reference_sequence(
106
+ references, start_number
107
+ )
108
+ all_references.extend(references)
109
+
110
+ messages.append(
111
+ f"{ARROW}**{item.name} {item.state.value}**: {message}"
112
+ )
113
+
114
+ await self.chat_service.modify_assistant_message_async(
115
+ content=self._progress_start_text + "\n\n" + "\n\n".join(messages),
116
+ references=all_references,
117
+ )
118
+
119
+ @staticmethod
120
+ def _replace_placeholders(message: str, start_number: int = 1) -> str:
121
+ counter = start_number
122
+
123
+ def replace_match(match):
124
+ nonlocal counter
125
+ result = f"<sup>{counter}</sup>"
126
+ counter += 1
127
+ return result
128
+
129
+ return re.sub(r"<sup></sup>", replace_match, message)
130
+
131
+ @staticmethod
132
+ def _correct_reference_sequence(
133
+ references: list[ContentReference], start_number: int = 1
134
+ ) -> list[ContentReference]:
135
+ for i, reference in enumerate(references, start_number):
136
+ reference.sequence_number = i
137
+ return references
138
+
139
+
140
+ class ToolWithToolProgressReporter(Protocol):
141
+ tool_progress_reporter: ToolProgressReporter
142
+
143
+
144
+ def track_tool_progress(
145
+ message: str,
146
+ on_start_state: ProgressState = ProgressState.RUNNING,
147
+ on_success_state: ProgressState = ProgressState.RUNNING,
148
+ on_success_message: str | None = None,
149
+ on_error_message: str = "Unexpected error occurred",
150
+ requires_new_assistant_message: bool = False,
151
+ ):
152
+ """
153
+ Decorator to add progress reporting and status tracking steps to tool functions. Can be used with async and sync functions.
154
+
155
+ Args:
156
+ name (str): Display name for the tool progress status
157
+ message (str): Message to show during tool execution
158
+ on_error_message (str, optional): Message to show if tool execution fails. Defaults to empty string.
159
+ on_success_state (ProgressState, optional): State to set after successful execution. Defaults to RUNNING.
160
+ requires_new_assistant_message (bool, optional): Whether to create a new assistant message. Defaults to False.
161
+
162
+ The decorator will:
163
+ 1. Show a RUNNING status when the tool starts executing
164
+ 2. Update the status to on_success_state if execution succeeds
165
+ 3. Update the status to FAILED if execution fails
166
+ 4. Include any references from the tool result in the status update if the result has a 'references' attribute or item.
167
+ 5. Create a new assistant message if requires_new_assistant_message is True
168
+
169
+ The decorated function must be a method of a class that implements ToolWithToolProgressReporter.
170
+ """
171
+
172
+ def decorator(func):
173
+ @wraps(func) # Preserve the original function's metadata
174
+ async def async_wrapper(
175
+ self: ToolWithToolProgressReporter,
176
+ tool_call: LanguageModelFunction,
177
+ notification_tool_name: str,
178
+ *args,
179
+ **kwargs,
180
+ ):
181
+ try:
182
+ # Start status
183
+ await self.tool_progress_reporter.notify_from_tool_call(
184
+ tool_call=tool_call,
185
+ name=notification_tool_name,
186
+ message=message,
187
+ state=on_start_state,
188
+ )
189
+
190
+ # Execute the tool function
191
+ result = await func(
192
+ self, tool_call, notification_tool_name, *args, **kwargs
193
+ )
194
+
195
+ # Success status
196
+ await self.tool_progress_reporter.notify_from_tool_call(
197
+ tool_call=tool_call,
198
+ name=notification_tool_name,
199
+ message=on_success_message or message,
200
+ state=on_success_state,
201
+ references=_get_references_from_results(result),
202
+ requires_new_assistant_message=requires_new_assistant_message,
203
+ )
204
+ return result
205
+
206
+ except Exception as e:
207
+ # Failure status
208
+ await self.tool_progress_reporter.notify_from_tool_call(
209
+ tool_call=tool_call,
210
+ name=notification_tool_name,
211
+ message=on_error_message,
212
+ state=ProgressState.FAILED,
213
+ requires_new_assistant_message=requires_new_assistant_message,
214
+ )
215
+ raise e
216
+
217
+ return async_wrapper
218
+
219
+ return decorator
220
+
221
+
222
+ def _get_references_from_results(result):
223
+ if isinstance(result, dict):
224
+ return result.get("references", [])
225
+ return getattr(result, "references", [])
File without changes