unisi 0.4.4__py3-none-any.whl → 0.4.6__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.
unisi/__init__.py CHANGED
@@ -1,8 +1,8 @@
1
1
  from .utils import *
2
- from .llmrag import Q
2
+ from .llmrag import Q, Qx
3
3
  from .units import *
4
4
  from .users import User
5
- from .server import start, handle, context_user, context_screen
5
+ from .server import start, handle, test, context_user, context_screen
6
6
  from .tables import *
7
7
  from .containers import *
8
8
  from .proxy import *
unisi/autotest.py CHANGED
@@ -181,7 +181,17 @@ def check_module(module):
181
181
  errors.insert(0, f"\nErrors in screen {screen.name}, file name {module.__file__}:")
182
182
  return errors
183
183
 
184
- def run_tests(user):
184
+ def run_tests(user):
185
+ if config.autotest:
186
+ for test_fn in Unishare.test_list:
187
+ try:
188
+ if asyncio.iscoroutinefunction(test_fn):
189
+ asyncio.run(test_fn())
190
+ else:
191
+ test_fn()
192
+ except Exception as e:
193
+ user.log(f'Test function {test_fn.__name__} failed with exception: {e}')
194
+
185
195
  errors = []
186
196
  for module in user.screens:
187
197
  module_errors = check_module(module)
unisi/llmrag.py CHANGED
@@ -3,6 +3,7 @@ from .common import Unishare
3
3
  from langchain_groq import ChatGroq
4
4
  from langchain_openai import ChatOpenAI
5
5
  from langchain_mistralai import ChatMistralAI
6
+ from langchain_xai import ChatXAI
6
7
  from langchain_google_genai import (
7
8
  ChatGoogleGenerativeAI,
8
9
  HarmBlockThreshold,
@@ -10,7 +11,7 @@ from langchain_google_genai import (
10
11
  )
11
12
  from datetime import datetime
12
13
  import os, inspect, re, json
13
- from typing import get_origin, get_args
14
+ from typing import get_origin, get_args, Any, Union
14
15
 
15
16
  class QueryCache:
16
17
  ITEM_SEPARATOR = "§¶†‡◊•→±"
@@ -102,24 +103,88 @@ def is_type(variable, expected_type):
102
103
  """
103
104
  Check if the variable matches the expected type hint.
104
105
  """
105
- origin = get_origin(expected_type)
106
- if origin is None:
107
- return isinstance(variable, expected_type)
106
+ # Handle explicit mapping of expected keys to types: {'name': str, 'age': int}
107
+ if isinstance(expected_type, dict):
108
+ if not isinstance(variable, dict):
109
+ return False
110
+ for key_pattern, sub_type in expected_type.items():
111
+ # direct key match
112
+ if key_pattern in variable:
113
+ if not is_type(variable[key_pattern], sub_type):
114
+ return False
115
+ continue
116
+ return True
117
+
118
+ # Handle typing hints (e.g., List[int], Dict[str,int], Union[..., ...], Optional[T], Tuple[T,...])
119
+ origin = get_origin(expected_type)
108
120
  args = get_args(expected_type)
109
-
110
- # Check if the type matches the generic type
111
- if not isinstance(variable, origin):
121
+
122
+ # No typing origin: expected_type may be a bare type or typing.Any
123
+ if origin is None:
124
+ # typing.Any
125
+ if expected_type is Any:
126
+ return True
127
+ # None as expected type
128
+ if expected_type is None:
129
+ return variable is None
130
+ # expected_type could be a builtin type or a tuple of types
131
+ if isinstance(expected_type, type) or isinstance(expected_type, tuple):
132
+ return isinstance(variable, expected_type)
133
+ # fallback: try isinstance; for other unexpected cases, be permissive
134
+ try:
135
+ return isinstance(variable, expected_type)
136
+ except Exception:
137
+ return False
138
+
139
+ # Handle Union / Optional
140
+ if origin is Union:
141
+ return any(is_type(variable, arg) for arg in args)
142
+
143
+ # Handle List[...] and Set[...]
144
+ if origin in (list, set):
145
+ expected_container = list if origin is list else set
146
+ if not isinstance(variable, expected_container):
147
+ return False
148
+ if not args:
149
+ return True
150
+ return all(is_type(item, args[0]) for item in variable)
151
+
152
+ # Handle Tuple[...] or tuple
153
+ if origin is tuple:
154
+ if not isinstance(variable, tuple):
155
+ return False
156
+ if not args:
157
+ return True
158
+ # Homogeneous tuple: Tuple[T, ...]
159
+ if len(args) == 2 and args[1] is Ellipsis:
160
+ return all(is_type(item, args[0]) for item in variable)
161
+ # Fixed-length tuple: Tuple[T1, T2, ...]
162
+ if len(variable) != len(args):
163
+ return False
164
+ return all(is_type(item, typ) for item, typ in zip(variable, args))
165
+
166
+ # Handle Dict[K, V]
167
+ if origin is dict:
168
+ if not isinstance(variable, dict):
169
+ return False
170
+ if not args:
171
+ return True
172
+ key_type, val_type = args
173
+ return all(is_type(k, key_type) and is_type(v, val_type) for k, v in variable.items())
174
+
175
+ # Handle Literal[...] (available in typing)
176
+ try:
177
+ from typing import Literal
178
+ if origin is Literal:
179
+ return any(variable == lit for lit in args)
180
+ except Exception:
181
+ pass
182
+
183
+ # Fallback: check using isinstance against the origin if possible
184
+ try:
185
+ return isinstance(variable, origin)
186
+ except Exception:
112
187
  return False
113
-
114
- if not args:
115
- return True
116
-
117
- if origin is list:
118
- return all(isinstance(item, args[0]) for item in variable)
119
- elif origin is dict:
120
- return all(isinstance(k, args[0]) and isinstance(v, args[1]) for k, v in variable.items())
121
-
122
- return False
123
188
 
124
189
  def remove_comments(json_str):
125
190
  # Regular expression to remove single-line comments (// ...)
@@ -128,17 +193,20 @@ def remove_comments(json_str):
128
193
  json_str = re.sub(r'/\*.*?\*/', '', json_str, flags=re.DOTALL)
129
194
  return json_str
130
195
 
131
- def Q(str_prompt, type_value = str, blank = True, **format_model):
132
- """returns LLM async call for a question"""
196
+ def Q(str_prompt, type_value = str, blank = True, extend = True, format = True, **format_model):
197
+ """returns LLM async call for a question, `extend = True` adds system prompt,
198
+ 'identity' in format_model can be used to set the assistant identity"""
133
199
  llm = Unishare.llm_model
134
- if '{' in str_prompt:
200
+ if format and '{' in str_prompt:
135
201
  caller_frame = inspect.currentframe().f_back
136
202
  format_model = caller_frame.f_locals | format_model if format_model else caller_frame.f_locals
137
203
  str_prompt = str_prompt.format(**format_model)
138
- if not re.search(r'json', str_prompt, re.IGNORECASE):
139
- jtype = jstype(type_value)
140
- format = " dd/mm/yyyy string" if type_value == 'date' else f'a JSON {jtype}' if jtype != 'string' else jtype
141
- str_prompt = f"System: You are an intelligent and extremely smart assistant. Output STRONGLY in format {format}. DO NOT OUTPUT ANY COMMENTARY." + str_prompt
204
+ if extend:
205
+ if type_value is not None:
206
+ jtype = jstype(type_value)
207
+ format = " dd/mm/yyyy string" if type_value == 'date' else f'a JSON {jtype}' if jtype != 'string' else jtype
208
+ str_prompt = f" Output STRONGLY in format {format}. DO NOT OUTPUT ANY COMMENTARY." + str_prompt
209
+ str_prompt = format_model.get('identity', 'You are an intelligent and extremely smart assistant.') + str_prompt
142
210
  async def f():
143
211
  if Unishare.llm_cache:
144
212
  if content := Unishare.llm_cache.get(str_prompt):
@@ -158,27 +226,16 @@ def Q(str_prompt, type_value = str, blank = True, **format_model):
158
226
  parsed = json.loads(clean_js)
159
227
  except json.JSONDecodeError as e:
160
228
  raise ValueError(f'Invalid JSON: {js}, \n Query: {str_prompt}')
161
- if isinstance(type_value, dict):
162
- for k, v in type_value.items():
163
- if k not in parsed:
164
- for k2, v2 in parsed.items():
165
- if re.fullmatch(k, k2, re.IGNORECASE) is not None:
166
- parsed[k] = parsed.pop(k2)
167
- break
168
- else:
169
- if blank:
170
- parsed[k] = None
171
- continue
172
- raise KeyError(f'Key {k} not found in {parsed}')
173
-
174
- if not is_type(parsed[k], v):
175
- raise TypeError(f'Invalid type for {k}: {type(parsed[k])} != {v}')
176
- else:
177
- if not is_type(parsed, type_value):
178
- raise TypeError(f'Invalid type: {type(parsed)} != {type_value}')
229
+
230
+ if not is_type(parsed, type_value):
231
+ raise TypeError(f'Invalid type: {type(parsed)} != {type_value}')
179
232
  return parsed
180
233
  return f()
181
234
 
235
+ def Qx(str_prompt, type_value = str):
236
+ """returns LLM async call for a question, without formatting or extending the prompt"""
237
+ return Q(str_prompt, type_value, format = False, extend = False)
238
+
182
239
  def setup_llmrag():
183
240
  import config #the module is loaded before config analysis
184
241
  temperature = getattr(config, 'temperature', 0.0)
@@ -197,6 +254,11 @@ def setup_llmrag():
197
254
  return
198
255
 
199
256
  type = type.lower()
257
+ model_kwargs={}
258
+ reasoning = getattr(config, 'reasoning', None)
259
+ if reasoning:
260
+ model_kwargs['reasoning'] = {"effort": reasoning, 'enabled': True}
261
+
200
262
  match type:
201
263
  case 'host':
202
264
  api_key_from_config = os.environ.get(api_key_config) if api_key_config else None
@@ -205,10 +267,21 @@ def setup_llmrag():
205
267
  api_key = api_key,
206
268
  temperature = temperature,
207
269
  openai_api_base = address,
270
+ model_kwargs=model_kwargs,
208
271
  model = model
209
272
  )
210
273
  case 'openai':
211
- Unishare.llm_model = ChatOpenAI(temperature = temperature)
274
+
275
+ Unishare.llm_model = ChatOpenAI(temperature = temperature, model_kwargs=model_kwargs)
276
+
277
+ case 'xai':
278
+ Unishare.llm_model = ChatXAI(
279
+ model = model,
280
+ temperature = temperature,
281
+ max_tokens = None,
282
+ timeout = None,
283
+ max_retries = 2,
284
+ )
212
285
 
213
286
  case 'groq':
214
287
  Unishare.llm_model = ChatGroq(
@@ -251,4 +324,3 @@ async def get_property(name, context = '', type = str, options = None):
251
324
  Unishare.message_logger(e)
252
325
  return None
253
326
  return value
254
-
unisi/server.py CHANGED
@@ -85,6 +85,12 @@ def handle(unit, event):
85
85
 
86
86
  Unishare.handle = handle
87
87
 
88
+ Unishare.test_list = []
89
+
90
+ def test(fn):
91
+ Unishare.test_list.append(fn)
92
+ return fn
93
+
88
94
  async def post_handler(request):
89
95
  reader = await request.multipart()
90
96
  field = await reader.next()
unisi/units.py CHANGED
@@ -49,7 +49,10 @@ class ChangedProxy:
49
49
  self._unit._mark_changed ()
50
50
 
51
51
  def __iter__(self):
52
- return iter(self._obj)
52
+ for item in self._obj:
53
+ if not callable(item) and not isinstance(item, atomics):
54
+ item = ChangedProxy(item, self._unit)
55
+ yield item
53
56
 
54
57
  def __len__(self):
55
58
  try:
@@ -69,8 +72,7 @@ class ChangedProxy:
69
72
 
70
73
  def __getstate__(self):
71
74
  return self._obj
72
-
73
- atomics = (int, float, complex, bool, str, bytes, ChangedProxy, type(None))
75
+
74
76
 
75
77
  class Unit:
76
78
  action_list = set(['complete', 'update', 'changed','delete','append', 'modify'])
@@ -108,6 +110,8 @@ class Unit:
108
110
  #it is correct condition order
109
111
  if name[0] != "_" and self._mark_changed:
110
112
  self._mark_changed(name, value)
113
+ if not callable(value) and not isinstance(value, atomics):
114
+ value = ChangedProxy(value, self)
111
115
  super().__setattr__(name, value)
112
116
 
113
117
  def mutate(self, obj):
@@ -169,6 +173,8 @@ class Unit:
169
173
 
170
174
  def __repr__(self):
171
175
  return f'{type(self).__name__}({self.name})'
176
+
177
+ atomics = (int, float, complex, bool, str, bytes, ChangedProxy, type(None), Unit)
172
178
 
173
179
  Line = Unit("__Line__", type = 'line')
174
180
 
unisi/users.py CHANGED
@@ -96,10 +96,10 @@ class User:
96
96
  setattr(screen, var, getattr(module, var, val))
97
97
  if not isinstance(screen.blocks, list):
98
98
  screen.blocks = [screen.blocks]
99
- if screen.toolbar:
99
+
100
+ if User.toolbar and User.toolbar[0] not in screen.toolbar:
100
101
  screen.toolbar += User.toolbar
101
- else:
102
- screen.toolbar = User.toolbar
102
+
103
103
  if User.count > 0:
104
104
  screen.set_reactivity(self)
105
105
  module.screen = screen#ChangedProxy(screen, screen)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: unisi
3
- Version: 0.4.4
3
+ Version: 0.4.6
4
4
  Summary: Unified System Interface, GUI and Remote API
5
5
  Author-Email: UNISI Tech <g.dernovoy@gmail.com>
6
6
  License: Apache-2.0
@@ -21,6 +21,7 @@ Requires-Dist: langchain-community
21
21
  Requires-Dist: langchain-openai
22
22
  Requires-Dist: langchain-google-genai
23
23
  Requires-Dist: langchain_mistralai
24
+ Requires-Dist: langchain-xai
24
25
  Requires-Dist: word2number
25
26
  Description-Content-Type: text/markdown
26
27
 
@@ -1,9 +1,9 @@
1
- unisi-0.4.4.dist-info/METADATA,sha256=tiL_tGRdP0YggAdpr7dh6YheOxvp1ImzRHgqscxijYc,27265
2
- unisi-0.4.4.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
3
- unisi-0.4.4.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
4
- unisi-0.4.4.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
5
- unisi/__init__.py,sha256=prG4FwJzpNJRX1trto0x_4Bne3kkpEX1dUxcRnIxWVw,301
6
- unisi/autotest.py,sha256=M6T8q1Yutqr1A1qUBBbdI3LpTlS7ejiGtfdZ9Yvyfqw,8942
1
+ unisi-0.4.6.dist-info/METADATA,sha256=y5IRgwr-ybfc7Aokz81UFXfCXFLPNOhPOaRB6RxcgnA,27294
2
+ unisi-0.4.6.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
3
+ unisi-0.4.6.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
4
+ unisi-0.4.6.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
5
+ unisi/__init__.py,sha256=XGnpUm_JhO5D41VSq_uEL-6hdtvwT8FZJ-pOVqApf9g,311
6
+ unisi/autotest.py,sha256=lQ22S9n01SWFc2dzpA3qgDriGLiz-hQRBrMc2UkZaFE,9324
7
7
  unisi/common.py,sha256=QHSS-pQDtLZxeZls0eDl8-EmYJZuaRmYO1M9izCly-Y,5791
8
8
  unisi/containers.py,sha256=yl5CZrTK3HmNJJxpO7SmzIIsBMehwfCb_otaWG7GriM,7416
9
9
  unisi/dbunits.py,sha256=g1h9pjoSwJrJ0Hmph-xv5jE1yfrxJeXGfuSnyVTg3j8,7438
@@ -14,14 +14,14 @@ unisi/jsoncomparison/config.py,sha256=LbdLJE1KIebFq_tX7zcERhPvopKhnzcTqMCnS3jN12
14
14
  unisi/jsoncomparison/errors.py,sha256=wqphE1Xn7K6n16uvUhDC45m2BxbsMUhIF2olPbhqf4o,1192
15
15
  unisi/jsoncomparison/ignore.py,sha256=xfF0a_BBEyGdZBoq-ovpCpawgcX8SRwwp7IrGnu1c2w,2634
16
16
  unisi/kdb.py,sha256=K-Lqc3e9hLTwO0i1ilTC6qrwZp90tXjLm7HFb_lM1Os,13621
17
- unisi/llmrag.py,sha256=ORITYgC--_QPyBRumoacQmUIsQG5d7UO10956l5Fg2c,9893
17
+ unisi/llmrag.py,sha256=HCEP8apy0FtIbLm0koG8BxCxjXS12Xqlo-9fZkGbsDs,12576
18
18
  unisi/multimon.py,sha256=YKwCuvMsMfdgOGkJoqiqh_9wywXMeo9bUhHmbAIUeSE,4060
19
19
  unisi/proxy.py,sha256=QMHSSFJtmVZIexIMAsuFNlF5JpnYNG90rkTM3PYJhY4,7750
20
20
  unisi/reloader.py,sha256=t7z0NgaeJX52044ue_LxITa99WMuE5Jra9qkMEeGhTg,6941
21
- unisi/server.py,sha256=zN2qGA2h-Bb3uJjUOAjkiRmmOIuDD6Ti_mNE62FTG_M,6667
21
+ unisi/server.py,sha256=BU8ERe0ewbJtuZeEGLqU30UMDFSENzO9xEh4BsQSejM,6759
22
22
  unisi/tables.py,sha256=1ZjR_eJ_vf0h26kdv2PjLV3t2cPMoqCV17MQjfilTX0,13810
23
- unisi/units.py,sha256=AGpb1Oad2HrNBaJNLR1Kl1OwjgbswAY7tQN195O7dn0,11691
24
- unisi/users.py,sha256=9incyR7QigCj1BENhZTNuXn8qC59OP2YONCKLtpZMtk,17082
23
+ unisi/units.py,sha256=5MVNJLFptVef1jKcDeDeKEPPti_t5Q5TrLeSt5xaaNk,11973
24
+ unisi/users.py,sha256=yq6rJzK62-NGcvwGIMgeRgBIGV0aDdngIwYgZxwnUII,17085
25
25
  unisi/utils.py,sha256=WD3O0hZzVQmcqQK6Ivv35kGmx4FC3enV1RtiZC_EndQ,2721
26
26
  unisi/voicecom.py,sha256=QzS1gIrBeGLO5dEwiu7KIEdJIIVbPBZFGb5nY632Ws8,16707
27
27
  unisi/web/css/262.46daf72a.css,sha256=esQA388EJR7xe_L5NwO30wvjDEuseTf9pqiacAjyAX8,3508
@@ -55,4 +55,4 @@ unisi/web/js/sigma.ce21336a.js,sha256=ngST-065XWOdnR_Xn7U6oGNHTL8fyiOEI9V8-BWRvl
55
55
  unisi/web/js/sigma.ce21336a.js.gz,sha256=zv6oToZZFCfmrZ4G4fw0sOncVe8-dyYNWh2v5QLKZp4,51965
56
56
  unisi/web/js/vendor.6a64dcc5.js,sha256=OSNK2nadU2DnSOEYQQcAmelybITOFZXMxnRyaDoT3yU,747104
57
57
  unisi/web/js/vendor.6a64dcc5.js.gz,sha256=nmtqRzQRWaToxgHxI9hfJd3UrUCg2-fd-0Fjc4H4wu8,245827
58
- unisi-0.4.4.dist-info/RECORD,,
58
+ unisi-0.4.6.dist-info/RECORD,,
File without changes