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.
Files changed (39) hide show
  1. unique_toolkit/__init__.py +0 -3
  2. unique_toolkit/{observability/log_config.py → app/init_logging.py} +10 -10
  3. unique_toolkit/app/init_sdk.py +41 -0
  4. unique_toolkit/{performance → app/performance}/async_executor.py +44 -17
  5. unique_toolkit/app/performance/async_wrapper.py +28 -0
  6. unique_toolkit/{event/schema.py → app/schemas.py} +17 -10
  7. unique_toolkit/app/verification.py +58 -0
  8. unique_toolkit/chat/schemas.py +30 -0
  9. unique_toolkit/chat/service.py +285 -87
  10. unique_toolkit/chat/state.py +22 -10
  11. unique_toolkit/chat/utils.py +25 -0
  12. unique_toolkit/content/schemas.py +90 -0
  13. unique_toolkit/content/service.py +356 -0
  14. unique_toolkit/content/utils.py +188 -0
  15. unique_toolkit/embedding/schemas.py +5 -0
  16. unique_toolkit/embedding/service.py +89 -0
  17. unique_toolkit/language_model/infos.py +305 -0
  18. unique_toolkit/language_model/schemas.py +168 -0
  19. unique_toolkit/language_model/service.py +261 -0
  20. unique_toolkit/language_model/utils.py +44 -0
  21. {unique_toolkit-0.0.1.dist-info → unique_toolkit-0.5.0.dist-info}/LICENSE +1 -1
  22. unique_toolkit-0.5.0.dist-info/METADATA +135 -0
  23. unique_toolkit-0.5.0.dist-info/RECORD +24 -0
  24. unique_toolkit/chat/__init__.py +0 -3
  25. unique_toolkit/chat/messages.py +0 -24
  26. unique_toolkit/event/__init__.py +0 -1
  27. unique_toolkit/event/constants.py +0 -1
  28. unique_toolkit/llm/__init__.py +0 -2
  29. unique_toolkit/llm/json_parser.py +0 -21
  30. unique_toolkit/llm/models.py +0 -37
  31. unique_toolkit/llm/service.py +0 -163
  32. unique_toolkit/performance/__init__.py +0 -1
  33. unique_toolkit/performance/async_wrapper.py +0 -13
  34. unique_toolkit/sdk/init.py +0 -19
  35. unique_toolkit/search/service.py +0 -118
  36. unique_toolkit/security/verify.py +0 -29
  37. unique_toolkit-0.0.1.dist-info/METADATA +0 -33
  38. unique_toolkit-0.0.1.dist-info/RECORD +0 -23
  39. {unique_toolkit-0.0.1.dist-info → unique_toolkit-0.5.0.dist-info}/WHEEL +0 -0
@@ -1,3 +0,0 @@
1
- from .observability.log_config import load_logging_config # noqa: F401
2
- from .sdk.init import init_sdk # noqa: F401
3
- from .security.verify import verify_signature_and_construct_event # noqa: F401
@@ -7,7 +7,7 @@ class UTCFormatter(Formatter):
7
7
  converter = gmtime
8
8
 
9
9
 
10
- log_config = {
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
- "formatters": {
20
- "utc": {
21
- "()": UTCFormatter,
22
- "format": "%(asctime)s: %(message)s",
23
- "datefmt": "%Y-%m-%d %H:%M:%S",
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 load_logging_config():
31
- return dictConfig(log_config)
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 Awaitable, Sequence, TypeVar, Union
7
-
8
- from quart import Quart
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
- def __init__(self, app_instance: Quart) -> None:
16
- self.app = app_instance
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 within one thread non-blocking and returns the results.
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.app.logger.info(
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.app.app_context():
70
+ async with self.context_manager():
44
71
  result = await task
45
72
  return result
46
73
  except Exception as e:
47
- self.app.logger.error(
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.app.logger.debug(
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 parallel threads and returns the results.
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.app.app_context():
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.app.logger.info(
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.app.logger.info(
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.app.logger.error(
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.app.logger.info(
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.app.logger.info(
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(alias_generator=camelize, populate_by_name=True)
8
+ model_config = ConfigDict(
9
+ alias_generator=camelize, populate_by_name=True, arbitrary_types_allowed=True
10
+ )
8
11
 
9
12
 
10
- class UserMessage(BaseModel):
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 AssistantMessage(BaseModel):
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 Payload(BaseModel):
32
+ class EventPayload(BaseModel):
26
33
  model_config = model_config
27
34
 
28
- name: str
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: UserMessage
34
- assistant_message: AssistantMessage
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: 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()