unique_toolkit 0.0.1__py3-none-any.whl → 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- unique_toolkit/__init__.py +0 -3
- unique_toolkit/{observability/log_config.py → app/init_logging.py} +10 -10
- unique_toolkit/app/init_sdk.py +41 -0
- unique_toolkit/{performance → app/performance}/async_executor.py +44 -17
- unique_toolkit/app/performance/async_wrapper.py +28 -0
- unique_toolkit/{event/schema.py → app/schemas.py} +17 -10
- unique_toolkit/app/verification.py +58 -0
- unique_toolkit/chat/schemas.py +30 -0
- unique_toolkit/chat/service.py +285 -87
- unique_toolkit/chat/state.py +22 -10
- unique_toolkit/chat/utils.py +25 -0
- unique_toolkit/content/schemas.py +90 -0
- unique_toolkit/content/service.py +356 -0
- unique_toolkit/content/utils.py +188 -0
- unique_toolkit/embedding/schemas.py +5 -0
- unique_toolkit/embedding/service.py +89 -0
- unique_toolkit/language_model/infos.py +305 -0
- unique_toolkit/language_model/schemas.py +168 -0
- unique_toolkit/language_model/service.py +261 -0
- unique_toolkit/language_model/utils.py +44 -0
- {unique_toolkit-0.0.1.dist-info → unique_toolkit-0.5.0.dist-info}/LICENSE +1 -1
- unique_toolkit-0.5.0.dist-info/METADATA +135 -0
- unique_toolkit-0.5.0.dist-info/RECORD +24 -0
- unique_toolkit/chat/__init__.py +0 -3
- unique_toolkit/chat/messages.py +0 -24
- unique_toolkit/event/__init__.py +0 -1
- unique_toolkit/event/constants.py +0 -1
- unique_toolkit/llm/__init__.py +0 -2
- unique_toolkit/llm/json_parser.py +0 -21
- unique_toolkit/llm/models.py +0 -37
- unique_toolkit/llm/service.py +0 -163
- unique_toolkit/performance/__init__.py +0 -1
- unique_toolkit/performance/async_wrapper.py +0 -13
- unique_toolkit/sdk/init.py +0 -19
- unique_toolkit/search/service.py +0 -118
- unique_toolkit/security/verify.py +0 -29
- unique_toolkit-0.0.1.dist-info/METADATA +0 -33
- unique_toolkit-0.0.1.dist-info/RECORD +0 -23
- {unique_toolkit-0.0.1.dist-info → unique_toolkit-0.5.0.dist-info}/WHEEL +0 -0
unique_toolkit/__init__.py
CHANGED
@@ -7,7 +7,7 @@ class UTCFormatter(Formatter):
|
|
7
7
|
converter = gmtime
|
8
8
|
|
9
9
|
|
10
|
-
|
10
|
+
unique_log_config = {
|
11
11
|
"version": 1,
|
12
12
|
"root": {"level": "DEBUG", "handlers": ["console"]},
|
13
13
|
"handlers": {
|
@@ -15,17 +15,17 @@ log_config = {
|
|
15
15
|
"class": "logging.StreamHandler",
|
16
16
|
"level": "DEBUG",
|
17
17
|
"formatter": "utc",
|
18
|
-
}
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
18
|
+
}
|
19
|
+
},
|
20
|
+
"formatters": {
|
21
|
+
"utc": {
|
22
|
+
"()": UTCFormatter,
|
23
|
+
"format": "%(asctime)s: %(message)s",
|
24
|
+
"datefmt": "%Y-%m-%d %H:%M:%S",
|
25
25
|
},
|
26
26
|
},
|
27
27
|
}
|
28
28
|
|
29
29
|
|
30
|
-
def
|
31
|
-
return dictConfig(
|
30
|
+
def init_logging(config: dict = unique_log_config):
|
31
|
+
return dictConfig(config)
|
@@ -0,0 +1,41 @@
|
|
1
|
+
import os
|
2
|
+
|
3
|
+
import unique_sdk
|
4
|
+
|
5
|
+
|
6
|
+
def get_env(var_name, default=None, strict=False):
|
7
|
+
"""Get the environment variable.
|
8
|
+
|
9
|
+
Args:
|
10
|
+
var_name (str): Name of the environment variable.
|
11
|
+
default (str, optional): Default value. Defaults to None.
|
12
|
+
strict (bool, optional): This method raises a ValueError, if strict, and no value is found in the environment. Defaults to False.
|
13
|
+
|
14
|
+
Raises:
|
15
|
+
ValueError: _description_
|
16
|
+
|
17
|
+
Returns:
|
18
|
+
_type_: _description_
|
19
|
+
"""
|
20
|
+
val = os.environ.get(var_name)
|
21
|
+
if not val:
|
22
|
+
if strict:
|
23
|
+
raise ValueError(f"{var_name} is not set")
|
24
|
+
return val or default
|
25
|
+
|
26
|
+
|
27
|
+
def init_sdk(strict_all_vars=False):
|
28
|
+
"""Initialize the SDK.
|
29
|
+
|
30
|
+
Args:
|
31
|
+
strict_all_vars (bool, optional): This method raises a ValueError if strict and no value is found in the environment. Defaults to False.
|
32
|
+
"""
|
33
|
+
unique_sdk.api_key = get_env("API_KEY", default="dummy", strict=strict_all_vars)
|
34
|
+
unique_sdk.app_id = get_env("APP_ID", default="dummy", strict=strict_all_vars)
|
35
|
+
unique_sdk.api_base = get_env("API_BASE", default=None, strict=strict_all_vars)
|
36
|
+
|
37
|
+
|
38
|
+
def get_endpoint_secret():
|
39
|
+
"""Fetch endpoint secret from the environment."""
|
40
|
+
endpoint_secret = os.getenv("ENDPOINT_SECRET")
|
41
|
+
return endpoint_secret
|
@@ -1,19 +1,46 @@
|
|
1
1
|
import asyncio
|
2
|
+
import contextlib
|
3
|
+
import logging
|
2
4
|
import threading
|
3
5
|
import time
|
4
6
|
from concurrent.futures import ThreadPoolExecutor
|
5
7
|
from math import ceil
|
6
|
-
from typing import
|
7
|
-
|
8
|
-
|
8
|
+
from typing import (
|
9
|
+
AsyncContextManager,
|
10
|
+
Awaitable,
|
11
|
+
Callable,
|
12
|
+
Optional,
|
13
|
+
Sequence,
|
14
|
+
TypeVar,
|
15
|
+
Union,
|
16
|
+
)
|
9
17
|
|
10
18
|
T = TypeVar("T")
|
11
19
|
Result = Union[T, BaseException]
|
12
20
|
|
13
21
|
|
14
22
|
class AsyncExecutor:
|
15
|
-
|
16
|
-
|
23
|
+
"""
|
24
|
+
A class for executing asynchronous tasks concurrently, with optional threading support.
|
25
|
+
|
26
|
+
This class provides methods to run multiple asynchronous tasks in parallel, with
|
27
|
+
the ability to limit the number of concurrent tasks and distribute work across
|
28
|
+
multiple threads if needed.
|
29
|
+
|
30
|
+
Attributes:
|
31
|
+
logger (logging.Logger): Logger instance for recording execution information.
|
32
|
+
context_manager (Callable[[], AsyncContextManager]): A factory function that returns
|
33
|
+
an async context manager to be used for each task execution, e.g., quart.current_app.app_context().
|
34
|
+
|
35
|
+
"""
|
36
|
+
|
37
|
+
def __init__(
|
38
|
+
self,
|
39
|
+
logger: Optional[logging.Logger] = None,
|
40
|
+
context_manager: Optional[Callable[[], AsyncContextManager]] = None,
|
41
|
+
) -> None:
|
42
|
+
self.logger = logger or logging.getLogger(__name__)
|
43
|
+
self.context_manager = context_manager or (lambda: contextlib.nullcontext())
|
17
44
|
|
18
45
|
async def run_async_tasks(
|
19
46
|
self,
|
@@ -21,7 +48,7 @@ class AsyncExecutor:
|
|
21
48
|
max_tasks: int,
|
22
49
|
) -> list[Result]:
|
23
50
|
"""
|
24
|
-
Executes the given async tasks
|
51
|
+
Executes the a set of given async tasks and returns the results.
|
25
52
|
|
26
53
|
Args:
|
27
54
|
tasks (list[Awaitable[T]]): list of async callables to execute in parallel.
|
@@ -35,23 +62,23 @@ class AsyncExecutor:
|
|
35
62
|
thread = threading.current_thread()
|
36
63
|
start_time = time.time()
|
37
64
|
|
38
|
-
self.
|
65
|
+
self.logger.info(
|
39
66
|
f"Thread {thread.name} (ID: {thread.ident}) starting task {task_id}"
|
40
67
|
)
|
41
68
|
|
42
69
|
try:
|
43
|
-
async with self.
|
70
|
+
async with self.context_manager():
|
44
71
|
result = await task
|
45
72
|
return result
|
46
73
|
except Exception as e:
|
47
|
-
self.
|
74
|
+
self.logger.error(
|
48
75
|
f"Thread {thread.name} (ID: {thread.ident}) - Task {task_id} failed with error: {e}"
|
49
76
|
)
|
50
77
|
return e
|
51
78
|
finally:
|
52
79
|
end_time = time.time()
|
53
80
|
duration = end_time - start_time
|
54
|
-
self.
|
81
|
+
self.logger.debug(
|
55
82
|
f"Thread {thread.name} (ID: {thread.ident}) - Task {task_id} finished in {duration:.2f} seconds"
|
56
83
|
)
|
57
84
|
|
@@ -78,7 +105,7 @@ class AsyncExecutor:
|
|
78
105
|
max_tasks: int,
|
79
106
|
) -> list[Result[T]]:
|
80
107
|
"""
|
81
|
-
Executes the given async tasks in
|
108
|
+
Executes the given async tasks in multiple threads and returns the results.
|
82
109
|
|
83
110
|
Args:
|
84
111
|
tasks (list[Awaitable[T]]): list of async callables to execute in parallel.
|
@@ -92,14 +119,14 @@ class AsyncExecutor:
|
|
92
119
|
async def run_in_thread(task_chunk: list[Awaitable[T]]) -> list[Result]:
|
93
120
|
loop = asyncio.new_event_loop()
|
94
121
|
asyncio.set_event_loop(loop)
|
95
|
-
async with self.
|
122
|
+
async with self.context_manager():
|
96
123
|
return await self.run_async_tasks(task_chunk, max_tasks)
|
97
124
|
|
98
125
|
def thread_worker(
|
99
126
|
task_chunk: list[Awaitable[T]], chunk_id: int
|
100
127
|
) -> list[Result]:
|
101
128
|
thread = threading.current_thread()
|
102
|
-
self.
|
129
|
+
self.logger.info(
|
103
130
|
f"Thread {thread.name} (ID: {thread.ident}) starting chunk {chunk_id} with {len(task_chunk)} tasks"
|
104
131
|
)
|
105
132
|
|
@@ -111,12 +138,12 @@ class AsyncExecutor:
|
|
111
138
|
results = loop.run_until_complete(run_in_thread(task_chunk))
|
112
139
|
end_time = time.time()
|
113
140
|
duration = end_time - start_time
|
114
|
-
self.
|
141
|
+
self.logger.info(
|
115
142
|
f"Thread {thread.name} (ID: {thread.ident}) finished chunk {chunk_id} in {duration:.2f} seconds"
|
116
143
|
)
|
117
144
|
return results
|
118
145
|
except Exception as e:
|
119
|
-
self.
|
146
|
+
self.logger.error(
|
120
147
|
f"Thread {thread.name} (ID: {thread.ident}) encountered an error in chunk {chunk_id}: {str(e)}"
|
121
148
|
)
|
122
149
|
raise
|
@@ -133,7 +160,7 @@ class AsyncExecutor:
|
|
133
160
|
for i in range(0, len(tasks), tasks_per_thread)
|
134
161
|
]
|
135
162
|
|
136
|
-
self.
|
163
|
+
self.logger.info(
|
137
164
|
f"Splitting {len(tasks)} tasks into {len(task_chunks)} chunks across {max_threads} threads"
|
138
165
|
)
|
139
166
|
|
@@ -152,7 +179,7 @@ class AsyncExecutor:
|
|
152
179
|
results: list[Result] = [item for sublist in future_results for item in sublist]
|
153
180
|
end_time = time.time()
|
154
181
|
duration = end_time - start_time
|
155
|
-
self.
|
182
|
+
self.logger.info(
|
156
183
|
f"All threads completed. Total results: {len(results)}. Duration: {duration:.2f} seconds"
|
157
184
|
)
|
158
185
|
|
@@ -0,0 +1,28 @@
|
|
1
|
+
import asyncio
|
2
|
+
import warnings
|
3
|
+
from functools import wraps
|
4
|
+
from typing import Any, Callable, Coroutine, TypeVar
|
5
|
+
|
6
|
+
T = TypeVar("T")
|
7
|
+
|
8
|
+
|
9
|
+
def to_async(func: Callable[..., T]) -> Callable[..., Coroutine[Any, Any, T]]:
|
10
|
+
@wraps(func)
|
11
|
+
async def wrapper(*args, **kwargs) -> T:
|
12
|
+
return await asyncio.to_thread(func, *args, **kwargs)
|
13
|
+
|
14
|
+
return wrapper
|
15
|
+
|
16
|
+
|
17
|
+
def async_warning(func):
|
18
|
+
@wraps(func)
|
19
|
+
async def wrapper(*args, **kwargs):
|
20
|
+
warnings.warn(
|
21
|
+
f"The function '{func.__name__}' is not purely async. It uses a thread pool executor underneath, "
|
22
|
+
"which may impact performance for CPU-bound operations.",
|
23
|
+
RuntimeWarning,
|
24
|
+
stacklevel=2,
|
25
|
+
)
|
26
|
+
return await func(*args, **kwargs)
|
27
|
+
|
28
|
+
return wrapper
|
@@ -1,13 +1,20 @@
|
|
1
|
+
from enum import StrEnum
|
1
2
|
from typing import Any
|
2
3
|
|
3
4
|
from humps import camelize
|
4
5
|
from pydantic import BaseModel, ConfigDict
|
5
6
|
|
6
7
|
# set config to convert camelCase to snake_case
|
7
|
-
model_config = ConfigDict(
|
8
|
+
model_config = ConfigDict(
|
9
|
+
alias_generator=camelize, populate_by_name=True, arbitrary_types_allowed=True
|
10
|
+
)
|
8
11
|
|
9
12
|
|
10
|
-
class
|
13
|
+
class EventName(StrEnum):
|
14
|
+
EXTERNAL_MODULE_CHOSEN = "unique.chat.external-module.chosen"
|
15
|
+
|
16
|
+
|
17
|
+
class EventUserMessage(BaseModel):
|
11
18
|
model_config = model_config
|
12
19
|
|
13
20
|
id: str
|
@@ -15,23 +22,23 @@ class UserMessage(BaseModel):
|
|
15
22
|
created_at: str
|
16
23
|
|
17
24
|
|
18
|
-
class
|
25
|
+
class EventAssistantMessage(BaseModel):
|
19
26
|
model_config = model_config
|
20
27
|
|
21
28
|
id: str
|
22
29
|
created_at: str
|
23
30
|
|
24
31
|
|
25
|
-
class
|
32
|
+
class EventPayload(BaseModel):
|
26
33
|
model_config = model_config
|
27
34
|
|
28
|
-
name:
|
35
|
+
name: EventName
|
29
36
|
description: str
|
30
37
|
configuration: dict[str, Any]
|
31
38
|
chat_id: str
|
32
39
|
assistant_id: str
|
33
|
-
user_message:
|
34
|
-
assistant_message:
|
40
|
+
user_message: EventUserMessage
|
41
|
+
assistant_message: EventAssistantMessage
|
35
42
|
text: str | None = None
|
36
43
|
|
37
44
|
|
@@ -39,9 +46,9 @@ class Event(BaseModel):
|
|
39
46
|
model_config = model_config
|
40
47
|
|
41
48
|
id: str
|
42
|
-
version: str
|
43
49
|
event: str
|
44
|
-
created_at: int
|
45
50
|
user_id: str
|
46
51
|
company_id: str
|
47
|
-
payload:
|
52
|
+
payload: EventPayload
|
53
|
+
created_at: int | None = None
|
54
|
+
version: str | None = None
|
@@ -0,0 +1,58 @@
|
|
1
|
+
import logging
|
2
|
+
|
3
|
+
import unique_sdk
|
4
|
+
|
5
|
+
from unique_toolkit.app.schemas import Event
|
6
|
+
|
7
|
+
|
8
|
+
class WebhookVerificationError(Exception):
|
9
|
+
"""Custom exception for webhook verification errors."""
|
10
|
+
|
11
|
+
pass
|
12
|
+
|
13
|
+
|
14
|
+
def verify_signature_and_construct_event(
|
15
|
+
headers: dict[str, str],
|
16
|
+
payload: bytes,
|
17
|
+
endpoint_secret: str,
|
18
|
+
logger: logging.Logger = logging.getLogger(__name__),
|
19
|
+
):
|
20
|
+
"""
|
21
|
+
Verify the signature of a webhook and construct an event object.
|
22
|
+
|
23
|
+
Args:
|
24
|
+
headers (Dict[str, str]): The headers of the webhook request.
|
25
|
+
payload (bytes): The raw payload of the webhook request.
|
26
|
+
endpoint_secret (str): The secret used to verify the webhook signature.
|
27
|
+
logger (logging.Logger): A logger instance for logging messages.
|
28
|
+
|
29
|
+
Returns:
|
30
|
+
Union[Event, Tuple[Dict[str, bool], int]]:
|
31
|
+
If successful, returns an Event object.
|
32
|
+
If unsuccessful, returns a tuple with an error response and HTTP status code.
|
33
|
+
|
34
|
+
Raises:
|
35
|
+
WebhookVerificationError: If there's an error during verification or event construction.
|
36
|
+
"""
|
37
|
+
|
38
|
+
# Only verify the event if there is an endpoint secret defined
|
39
|
+
# Otherwise use the basic event deserialized with json
|
40
|
+
sig_header = headers.get("X-Unique-Signature")
|
41
|
+
timestamp = headers.get("X-Unique-Created-At")
|
42
|
+
|
43
|
+
if not sig_header or not timestamp:
|
44
|
+
logger.error("⚠️ Webhook signature or timestamp headers missing.")
|
45
|
+
raise WebhookVerificationError("Signature or timestamp headers missing")
|
46
|
+
|
47
|
+
try:
|
48
|
+
event = unique_sdk.Webhook.construct_event(
|
49
|
+
payload,
|
50
|
+
sig_header,
|
51
|
+
timestamp,
|
52
|
+
endpoint_secret,
|
53
|
+
)
|
54
|
+
logger.info("✅ Webhook signature verification successful.")
|
55
|
+
return Event(**event)
|
56
|
+
except unique_sdk.SignatureVerificationError as e:
|
57
|
+
logger.error("⚠️ Webhook signature verification failed. " + str(e))
|
58
|
+
raise WebhookVerificationError(f"Signature verification failed: {str(e)}")
|
@@ -0,0 +1,30 @@
|
|
1
|
+
from enum import Enum
|
2
|
+
|
3
|
+
from humps import camelize
|
4
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
5
|
+
|
6
|
+
# set config to convert camelCase to snake_case
|
7
|
+
model_config = ConfigDict(
|
8
|
+
alias_generator=camelize, populate_by_name=True, arbitrary_types_allowed=True
|
9
|
+
)
|
10
|
+
|
11
|
+
|
12
|
+
class ChatMessageRole(str, Enum):
|
13
|
+
USER = "user"
|
14
|
+
ASSISTANT = "assistant"
|
15
|
+
|
16
|
+
|
17
|
+
class ChatMessage(BaseModel):
|
18
|
+
model_config = model_config
|
19
|
+
|
20
|
+
id: str | None = None
|
21
|
+
object: str | None = None
|
22
|
+
content: str = Field(alias="text")
|
23
|
+
role: ChatMessageRole
|
24
|
+
debug_info: dict = {}
|
25
|
+
|
26
|
+
# TODO make sdk return role consistently in lowercase
|
27
|
+
# Currently needed as sdk returns role in uppercase
|
28
|
+
@field_validator("role", mode="before")
|
29
|
+
def set_role(cls, value: str):
|
30
|
+
return value.lower()
|