uvai-tools 0.1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 uvme01
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.4
2
+ Name: uvai-tools
3
+ Version: 0.1.0
4
+ Summary: Тег-команды (<calculate>, <date>, <search>, <fix_tags>...) и универсальная обвязка connect/system/system_stream для подключения любой генеративной модели
5
+ Author-email: uvme01 <maksimu964@gmail.com>
6
+ License: MIT
7
+ Keywords: ai,llm,tools,tool-calling,agent
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Topic :: Software Development :: Libraries
12
+ Requires-Python: >=3.9
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: python-dateutil>=2.8
16
+ Provides-Extra: search
17
+ Requires-Dist: ddgs>=9.0; extra == "search"
18
+ Requires-Dist: beautifulsoup4>=4.12; extra == "search"
19
+ Dynamic: license-file
20
+
21
+ # uvai-tools
22
+
23
+ Набор тег-команд для ИИ-ассистентов (`<calculate>`, `<date>`, `<time>`,
24
+ `<search>`, `<fix_tags>`, `<layout>`) плюс универсальная обвязка
25
+ `connect` / `system` / `system_stream` для подключения **любой**
26
+ генеративной модели — независимо от формата истории сообщений и
27
+ сигнатуры вашей функции генерации.
28
+
29
+ ## Установка
30
+
31
+ ```bash
32
+ pip install uvai-tools
33
+
34
+ # если нужен тег <search> (веб-поиск + парсинг страниц)
35
+ pip install "uvai-tools[search]"
36
+ ```
37
+
38
+ ## Быстрый старт
39
+
40
+ ```python
41
+ from tools import connect
42
+
43
+ def my_generate(history):
44
+ # ваш вызов модели, например:
45
+ # response = client.messages.create(messages=history, ...)
46
+ # return response.content[0].text
47
+ ...
48
+
49
+ result = connect("Посчитай <calculate 2+2>", my_generate)
50
+ ```
51
+
52
+ ## Поддерживаемые теги
53
+
54
+ | Тег | Что делает |
55
+ |---|---|
56
+ | `<calculate 2+2>` | Вычисляет выражение |
57
+ | `<date>`, `<date +1y>` | Текущая/сдвинутая дата |
58
+ | `<time>`, `<time +30m>` | Текущее/сдвинутое время |
59
+ | `<search запрос>` | Веб-поиск + краткая выжимка со страниц (нужен extra `search`) |
60
+ | `<fix_tags>` | Автоматически закрывает/выравнивает незакрытые HTML-теги в тексте до этого места |
61
+ | `<layout текст>` | Переключает раскладку текста RU⇄EN |
62
+
63
+ ## Кастомизация под свой ИИ
64
+
65
+ `connect` / `system` / `system_stream` работают из коробки со стандартным
66
+ форматом `{"role": ..., "content": ...}`, но принимают параметры для
67
+ любого другого формата сообщений, нестандартной сигнатуры функции
68
+ генерации или async-моделей. Подробности — в докстрингах функций.
69
+
70
+ ## Лицензия
71
+
72
+ MIT
@@ -0,0 +1,52 @@
1
+ # uvai-tools
2
+
3
+ Набор тег-команд для ИИ-ассистентов (`<calculate>`, `<date>`, `<time>`,
4
+ `<search>`, `<fix_tags>`, `<layout>`) плюс универсальная обвязка
5
+ `connect` / `system` / `system_stream` для подключения **любой**
6
+ генеративной модели — независимо от формата истории сообщений и
7
+ сигнатуры вашей функции генерации.
8
+
9
+ ## Установка
10
+
11
+ ```bash
12
+ pip install uvai-tools
13
+
14
+ # если нужен тег <search> (веб-поиск + парсинг страниц)
15
+ pip install "uvai-tools[search]"
16
+ ```
17
+
18
+ ## Быстрый старт
19
+
20
+ ```python
21
+ from tools import connect
22
+
23
+ def my_generate(history):
24
+ # ваш вызов модели, например:
25
+ # response = client.messages.create(messages=history, ...)
26
+ # return response.content[0].text
27
+ ...
28
+
29
+ result = connect("Посчитай <calculate 2+2>", my_generate)
30
+ ```
31
+
32
+ ## Поддерживаемые теги
33
+
34
+ | Тег | Что делает |
35
+ |---|---|
36
+ | `<calculate 2+2>` | Вычисляет выражение |
37
+ | `<date>`, `<date +1y>` | Текущая/сдвинутая дата |
38
+ | `<time>`, `<time +30m>` | Текущее/сдвинутое время |
39
+ | `<search запрос>` | Веб-поиск + краткая выжимка со страниц (нужен extra `search`) |
40
+ | `<fix_tags>` | Автоматически закрывает/выравнивает незакрытые HTML-теги в тексте до этого места |
41
+ | `<layout текст>` | Переключает раскладку текста RU⇄EN |
42
+
43
+ ## Кастомизация под свой ИИ
44
+
45
+ `connect` / `system` / `system_stream` работают из коробки со стандартным
46
+ форматом `{"role": ..., "content": ...}`, но принимают параметры для
47
+ любого другого формата сообщений, нестандартной сигнатуры функции
48
+ генерации или async-моделей. Подробности — в докстрингах функций.
49
+
50
+ ## Лицензия
51
+
52
+ MIT
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "uvai-tools"
7
+ version = "0.1.0"
8
+ description = "Тег-команды (<calculate>, <date>, <search>, <fix_tags>...) и универсальная обвязка connect/system/system_stream для подключения любой генеративной модели"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "uvme01", email = "maksimu964@gmail.com" },
14
+ ]
15
+ keywords = ["ai", "llm", "tools", "tool-calling", "agent"]
16
+ classifiers = [
17
+ "Programming Language :: Python :: 3",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: OS Independent",
20
+ "Topic :: Software Development :: Libraries",
21
+ ]
22
+ dependencies = [
23
+ "python-dateutil>=2.8",
24
+ ]
25
+
26
+ [project.optional-dependencies]
27
+ # нужны только для тега <search> (интернет-поиск и парсинг страниц)
28
+ search = [
29
+ "ddgs>=9.0",
30
+ "beautifulsoup4>=4.12",
31
+ ]
32
+
33
+ [tool.setuptools.packages.find]
34
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,41 @@
1
+ """
2
+ ai_tag_tools
3
+ ============
4
+
5
+ Набор инструментов для ИИ-ассистентов: разбор и выполнение
6
+ тег-команд вида <calculate>, <date>, <time>, <search>, <fix_tags>,
7
+ <layout>, плюс универсальные обвязки connect/system/system_stream
8
+ для подключения любой генеративной модели.
9
+ """
10
+
11
+ from .core import (
12
+ tools_patterns,
13
+ check_for_toolcall,
14
+ connect,
15
+ system,
16
+ system_stream,
17
+ use_all,
18
+ calculator,
19
+ get_date,
20
+ get_time,
21
+ change_layout,
22
+ search_web,
23
+ fix_html_tags,
24
+ )
25
+
26
+ __version__ = "0.1.0"
27
+
28
+ __all__ = [
29
+ "tools_patterns",
30
+ "check_for_toolcall",
31
+ "connect",
32
+ "system",
33
+ "system_stream",
34
+ "use_all",
35
+ "calculator",
36
+ "get_date",
37
+ "get_time",
38
+ "change_layout",
39
+ "search_web",
40
+ "fix_html_tags",
41
+ ]
@@ -0,0 +1,478 @@
1
+ import re
2
+ import urllib.request as ur
3
+
4
+ import types
5
+ import textwrap
6
+
7
+ from datetime import datetime, timedelta
8
+ from dateutil.relativedelta import relativedelta
9
+
10
+ tools_patterns = {
11
+ r"<calculate(?:\s+(.*?))?>": "calculator",
12
+
13
+ r"<date(?:\s+(.*?))?>": "get_date",
14
+ r"<time(?:\s+(.*?))?>": "get_time",
15
+
16
+ r"<search(?:\s+(.*?))?>": "search_web",
17
+ r"<fix_tags>": "fix_html_tags",
18
+ r"<layout(?:\s+(.*?))?>": "change_layout",
19
+ }
20
+
21
+
22
+ def check_for_toolcall(answer: str) -> bool:
23
+ for pattern in tools_patterns.keys():
24
+ match = re.search(pattern, answer)
25
+ if match:
26
+ return True
27
+ return False
28
+
29
+ def _default_get_role(msg):
30
+ return msg["role"]
31
+
32
+ def _default_get_content(msg):
33
+ return msg["content"]
34
+
35
+ def _default_set_content(msg, content):
36
+ msg["content"] = content
37
+ return msg
38
+
39
+ def _default_make_message(role, content):
40
+ return {"role": role, "content": content}
41
+
42
+ def connect(
43
+ input_data,
44
+ generate_function,
45
+ *,
46
+ max_iterations: int = 5,
47
+ user_role: str = "user",
48
+ assistant_role: str = "assistant",
49
+ get_role=_default_get_role,
50
+ get_content=_default_get_content,
51
+ set_content=_default_set_content,
52
+ make_message=_default_make_message,
53
+ call_generate=None,
54
+ is_async: bool = False,
55
+ ):
56
+ """
57
+ Универсальный вход для любого ИИ-бэкенда.
58
+
59
+ input_data:
60
+ - str -> одноразовый запрос, вернёт итоговую строку ответа
61
+ - list -> история сообщений, вернёт обновлённый список истории
62
+ - generator/func -> потоковая генерация, вернёт генератор чанков
63
+
64
+ generate_function: то, что реально дёргает вашу модель. Сигнатура зависит
65
+ от call_generate (см. ниже). По умолчанию ожидается generate_function(history) -> str.
66
+
67
+ Если формат сообщений/вызова генератора у вас нестандартный - передайте:
68
+ get_role(msg) -> str - достать роль из сообщения
69
+ get_content(msg) -> str - достать текст из сообщения
70
+ set_content(msg, content) - записать текст в сообщение (для системы стрима)
71
+ make_message(role, content) - создать новое сообщение в вашем формате
72
+ call_generate(generate_function, history) -> str
73
+ - как именно вызывать вашу модель
74
+ (например, лямбда, распаковывающая
75
+ историю в другой формат, добавляющая
76
+ await, доп. параметры и т.д.)
77
+ max_iterations - сколько раз подряд можно вызывать тулы
78
+ user_role / assistant_role - имена ролей в вашей истории
79
+
80
+ is_async=True - если generate_function / call_generate возвращают awaitable
81
+ (тогда используйте connect в связке с await, либо передайте свой
82
+ call_generate, оборачивающий асинхронный вызов синхронно, например через
83
+ asyncio.run).
84
+ """
85
+ if call_generate is None:
86
+ call_generate = lambda gen_func, history: gen_func(history)
87
+
88
+ common_kwargs = dict(
89
+ max_iterations=max_iterations,
90
+ user_role=user_role,
91
+ assistant_role=assistant_role,
92
+ get_role=get_role,
93
+ get_content=get_content,
94
+ set_content=set_content,
95
+ make_message=make_message,
96
+ call_generate=call_generate,
97
+ is_async=is_async,
98
+ )
99
+
100
+ if isinstance(input_data, (types.GeneratorType, types.FunctionType)) or callable(input_data):
101
+ return system_stream(input_data)
102
+ elif isinstance(input_data, list):
103
+ return system(input_data, generate_function, **common_kwargs)
104
+ elif isinstance(input_data, str):
105
+ mock_history = [make_message(assistant_role, input_data)]
106
+ res_history = system(mock_history, generate_function, **common_kwargs)
107
+ return get_content(res_history[-1])
108
+ return input_data
109
+
110
+ def system(
111
+ history: list,
112
+ generate_function,
113
+ *,
114
+ max_iterations: int = 5,
115
+ user_role: str = "user",
116
+ assistant_role: str = "assistant",
117
+ get_role=_default_get_role,
118
+ get_content=_default_get_content,
119
+ set_content=_default_set_content,
120
+ make_message=_default_make_message,
121
+ call_generate=None,
122
+ is_async: bool = False,
123
+ ) -> list:
124
+ if call_generate is None:
125
+ call_generate = lambda gen_func, hist: gen_func(hist)
126
+
127
+ if not history:
128
+ return history
129
+
130
+ iteration = 0
131
+
132
+ while (
133
+ iteration < max_iterations
134
+ and any(re.search(pat, get_content(history[-1])) for pat in tools_patterns.keys())
135
+ ):
136
+ raw_ai_output = get_content(history[-1])
137
+
138
+ found_tag = None
139
+ for pat in tools_patterns.keys():
140
+ match = re.search(pat, raw_ai_output)
141
+ if match:
142
+ found_tag = match.group(0)
143
+ break
144
+
145
+ if not found_tag:
146
+ break
147
+
148
+ tool_result = use_all(found_tag)
149
+ result_block = f"{found_tag} -> {tool_result}"
150
+
151
+ history.append(make_message(user_role, result_block))
152
+
153
+ next_ai_output = call_generate(generate_function, history)
154
+ if is_async and hasattr(next_ai_output, "__await__"):
155
+ raise TypeError(
156
+ "generate_function вернул awaitable, но is_async обработан не был. "
157
+ "Передайте call_generate, который сам разворачивает корутину "
158
+ "(например через asyncio.run(...) или свой event loop)."
159
+ )
160
+ history.append(make_message(assistant_role, next_ai_output))
161
+ iteration += 1
162
+
163
+ return history
164
+
165
+ def system_stream(
166
+ generator,
167
+ *,
168
+ call_generator=None,
169
+ tag_pattern=r"(</?([a-zA-Z0-9_]+)(?:\s+[^>]*)?>)",
170
+ ):
171
+ """
172
+ Универсальный потоковый обработчик.
173
+
174
+ generator: генератор чанков текста, ИЛИ функция без обязательных
175
+ аргументов, которая возвращает такой генератор (например
176
+ `lambda: client.stream(prompt)`).
177
+
178
+ Если ваша функция генерации требует аргументы (промпт, историю,
179
+ доп. параметры) - передайте их через call_generator, например:
180
+ system_stream(lambda: my_stream_call(prompt, history))
181
+ или сразу передайте готовый генератор/итератор чанков в generator.
182
+
183
+ tag_pattern - если ваши теги отличаются от `<name attrs>` /
184
+ `</name>`, можно передать свой регекс с той же структурой групп.
185
+ """
186
+ if callable(generator) and not isinstance(generator, types.GeneratorType):
187
+ generator = call_generator(generator) if call_generator else generator()
188
+
189
+ buffer = ""
190
+ inside_tag = False
191
+
192
+ for chunk in generator:
193
+ buffer += chunk
194
+
195
+ if "<" in buffer and not inside_tag:
196
+ clean_part, tag_start = buffer.split("<", 1)
197
+ if clean_part:
198
+ yield clean_part
199
+ buffer = "<" + tag_start
200
+ inside_tag = True
201
+
202
+ if inside_tag and ">" in buffer:
203
+ tag_match = re.search(tag_pattern, buffer)
204
+
205
+ if tag_match:
206
+ full_tag = tag_match.group(1)
207
+ is_tool = any(re.search(pat, full_tag) for pat in tools_patterns.keys())
208
+
209
+ if is_tool:
210
+ tool_result = use_all(full_tag)
211
+ result_block = f"{full_tag} -> {tool_result}"
212
+ yield result_block
213
+
214
+ buffer = buffer.replace(full_tag, "", 1)
215
+ inside_tag = False
216
+ else:
217
+ yield full_tag
218
+ buffer = buffer.replace(full_tag, "", 1)
219
+ if "<" not in buffer:
220
+ inside_tag = False
221
+
222
+ if not inside_tag and buffer:
223
+ yield buffer
224
+ buffer = ""
225
+
226
+ if buffer:
227
+ yield fix_html_tags(buffer)
228
+
229
+ def use_all(answer: str) -> str:
230
+ try:
231
+ for pattern in tools_patterns.keys():
232
+ match = re.search(pattern, answer)
233
+ if match:
234
+ func_name = tools_patterns.get(pattern)
235
+ func = globals()[func_name]
236
+ answer = func(answer)
237
+ return answer
238
+ except Exception as e:
239
+ raise(e)
240
+ return answer
241
+
242
+ def calculator(answer: str) -> str:
243
+ pattern = list(tools_patterns.keys())[0]
244
+ matches = re.findall(pattern, answer)
245
+
246
+ for match in matches:
247
+ result = str(eval(match))
248
+ answer = answer.replace(f"<calculate {match}>", result)
249
+ return answer
250
+
251
+ def get_date(answer: str) -> str:
252
+
253
+ def match_handler(match):
254
+ params = match.group(1) if match.group(1) else ""
255
+ params = params.strip()
256
+
257
+ now = datetime.today()
258
+
259
+ if not params:
260
+ return now.strftime("%d.%m.%Y")
261
+ if params in ['y', 'M', 'd']:
262
+ if params == 'y':
263
+ return now.year
264
+ elif params == 'M':
265
+ return now.month
266
+ elif params == 'd':
267
+ return now.day
268
+
269
+ shifts = re.findall(r'([+-]?\d+)([dMy])', params)
270
+
271
+ if not shifts:
272
+ return now.strftime("%d.%m.%Y")
273
+
274
+ total_days = 0
275
+ total_months = 0
276
+ total_years = 0
277
+
278
+ for value, unit in shifts:
279
+ value = int(value)
280
+ if unit == 'y':
281
+ total_years += value
282
+ elif unit == 'M':
283
+ total_months += value
284
+ elif unit == 'd':
285
+ total_days += value
286
+
287
+ future_date = now + relativedelta(
288
+ years=total_years, months=total_months,
289
+ days=total_days
290
+ )
291
+ return future_date.strftime("%d.%m.%Y")
292
+
293
+ pattern = pattern = list(tools_patterns.keys())[1]
294
+ return re.sub(pattern, match_handler, answer)
295
+
296
+ def get_time(text: str) -> str:
297
+
298
+ def match_handler(match):
299
+ params = match.group(1) if match.group(1) else ""
300
+ params = params.strip()
301
+
302
+ now = datetime.now()
303
+
304
+ if not params:
305
+ return now.strftime("%H:%M:%S")
306
+ if params in ['h', 'm', 's']:
307
+ if params == 'h':
308
+ return now.strftime("%H")
309
+ elif params == 'm':
310
+ return now.strftime("%M")
311
+ elif params == 's':
312
+ return now.strftime("%S")
313
+
314
+ shifts = re.findall(r'([+-]?\d+)([hm])', params)
315
+
316
+ if not shifts:
317
+ return now.strftime("%H:%M:%S")
318
+
319
+ total_hours = 0
320
+ total_minutes = 0
321
+
322
+ for value, unit in shifts:
323
+ value = int(value)
324
+ if unit == 'h':
325
+ total_hours += value
326
+ elif unit == 'm':
327
+ total_minutes += value
328
+
329
+ future_time = now + timedelta(hours=total_hours, minutes=total_minutes)
330
+ return future_time.strftime("%H:%M:%S")
331
+
332
+ pattern = list(tools_patterns.keys())[2]
333
+ return re.sub(pattern, match_handler, text)
334
+
335
+ def change_layout(answer: str) -> str:
336
+
337
+ en_chars = "qwertyuiop[]asdfghjkl;'zxcvbnm,.QWERTYUIOP{}ASDFGHJKL:\"ZXCVBNM<>"
338
+ ru_chars = "йцукенгшщзхъфывапролджэячсмитьбюЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮ"
339
+ en_to_ru = str.maketrans(en_chars, ru_chars)
340
+ ru_to_en = str.maketrans(ru_chars, en_chars)
341
+
342
+ def match_handler(match):
343
+ params = match.group(1) if match.group(1) else ""
344
+ params = params.strip()
345
+ if not params:
346
+ return params
347
+
348
+ fixed = params.translate(en_to_ru) if (params[0] in en_chars) else params.translate(ru_to_en)
349
+ return fixed
350
+
351
+ pattern = list(tools_patterns.keys())[5]
352
+ return re.sub(pattern, match_handler, answer)
353
+
354
+
355
+ def search_web(answer: str) -> str:
356
+ def match_handler(match):
357
+ query = match.group(1) if match.group(1) else ""
358
+ query = query.strip()
359
+ query = use_all(query)
360
+
361
+ if not query:
362
+ return "[search: пустой запрос]"
363
+
364
+ try:
365
+ try:
366
+ from ddgs import DDGS #type:ignore
367
+ except ImportError:
368
+ from duckduckgo_search import DDGS #type:ignore
369
+
370
+ with DDGS() as ddgs:
371
+ results = list(ddgs.text(query, max_results=3))
372
+ except Exception as e:
373
+ return f"[search error: {e}]"
374
+
375
+ if not results:
376
+ return f"[search: по запросу «{query}» ничего не найдено]"
377
+
378
+ links = []
379
+ lines = []
380
+ for r in results:
381
+ body = (r.get("body") or "").strip()
382
+ href = (r.get("href") or "").strip()
383
+ links.append(href)
384
+ lines.append(f"Information:\n{body[30:80] + "..." if len(body) >= 80 else body[:50] + "..."}")
385
+ texts = _deep_search(links)
386
+ result = textwrap.shorten(f'{"\n".join(lines)}\nDetails:\n{texts}\n', width=800, placeholder="...")
387
+ print(len(result))
388
+ return result
389
+
390
+ pattern = list(tools_patterns.keys())[3]
391
+ return re.sub(pattern, match_handler, answer)
392
+
393
+ def _deep_search(urls: list) -> str:
394
+ import urllib.parse as up
395
+ from bs4 import BeautifulSoup as bs
396
+ headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}
397
+
398
+ texts = []
399
+ need_one = int(300 / len(urls))
400
+ start = 100
401
+ end = start + need_one
402
+
403
+ for url in urls:
404
+ safe_url = up.quote(url, safe=':/?&=')
405
+
406
+ req = ur.Request(
407
+ safe_url,
408
+ headers=headers
409
+ )
410
+ try:
411
+ with ur.urlopen(req, timeout=3) as response:
412
+ raw = response.read().decode("utf-8")
413
+ soup = bs(raw, 'html.parser')
414
+ text = soup.get_text(separator=' ', strip=True)
415
+ texts.append(text[start:end] + "...")
416
+ start += int(len(text) / need_one)
417
+ end = start + end
418
+ except Exception as e:
419
+ texts.append("Страница недоступна")
420
+ return "\n".join(texts)
421
+
422
+ def fix_html_tags(answer: str) -> str:
423
+ answer = answer.replace("<fix_tags>", "")
424
+ single_tags = {"img", "br", "input", "hr", "meta", "link", "area", "base", "col", "embed"}
425
+
426
+ stack = []
427
+ fixed_chunks = []
428
+ last_idx = 0
429
+
430
+ for match in re.finditer(r"<[^>]+>", answer):
431
+ start, end = match.span()
432
+ tag_text = match.group(0)
433
+
434
+ fixed_chunks.append(answer[last_idx:start])
435
+ last_idx = end
436
+
437
+ is_closing = tag_text.startswith("</")
438
+ is_self_closing = tag_text.endswith("/>")
439
+
440
+ name_match = re.search(r"<\/*([a-zA-Z1-6]+)", tag_text)
441
+ if not name_match:
442
+ fixed_chunks.append(tag_text)
443
+ continue
444
+
445
+ tag_name = name_match.group(1).lower()
446
+
447
+ if tag_name in single_tags or is_self_closing:
448
+ fixed_chunks.append(tag_text)
449
+ continue
450
+
451
+ if not is_closing:
452
+ stack.append(tag_name)
453
+ fixed_chunks.append(tag_text)
454
+ else:
455
+ if stack:
456
+ if stack[-1] == tag_name:
457
+ stack.pop()
458
+ fixed_chunks.append(tag_text)
459
+ else:
460
+ correction = ""
461
+ while stack and stack[-1] != tag_name:
462
+ missed_tag = stack.pop()
463
+ correction += f"</{missed_tag}>"
464
+
465
+ if stack and stack[-1] == tag_name:
466
+ stack.pop()
467
+
468
+ fixed_chunks.append(correction + tag_text)
469
+ else:
470
+ pass
471
+
472
+ fixed_chunks.append(answer[last_idx:])
473
+ end_correction = ""
474
+ while len(stack) != 0:
475
+ missed_tag = stack.pop()
476
+ end_correction += f"</{missed_tag}>"
477
+
478
+ return "".join(fixed_chunks) + end_correction
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.4
2
+ Name: uvai-tools
3
+ Version: 0.1.0
4
+ Summary: Тег-команды (<calculate>, <date>, <search>, <fix_tags>...) и универсальная обвязка connect/system/system_stream для подключения любой генеративной модели
5
+ Author-email: uvme01 <maksimu964@gmail.com>
6
+ License: MIT
7
+ Keywords: ai,llm,tools,tool-calling,agent
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Topic :: Software Development :: Libraries
12
+ Requires-Python: >=3.9
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: python-dateutil>=2.8
16
+ Provides-Extra: search
17
+ Requires-Dist: ddgs>=9.0; extra == "search"
18
+ Requires-Dist: beautifulsoup4>=4.12; extra == "search"
19
+ Dynamic: license-file
20
+
21
+ # uvai-tools
22
+
23
+ Набор тег-команд для ИИ-ассистентов (`<calculate>`, `<date>`, `<time>`,
24
+ `<search>`, `<fix_tags>`, `<layout>`) плюс универсальная обвязка
25
+ `connect` / `system` / `system_stream` для подключения **любой**
26
+ генеративной модели — независимо от формата истории сообщений и
27
+ сигнатуры вашей функции генерации.
28
+
29
+ ## Установка
30
+
31
+ ```bash
32
+ pip install uvai-tools
33
+
34
+ # если нужен тег <search> (веб-поиск + парсинг страниц)
35
+ pip install "uvai-tools[search]"
36
+ ```
37
+
38
+ ## Быстрый старт
39
+
40
+ ```python
41
+ from tools import connect
42
+
43
+ def my_generate(history):
44
+ # ваш вызов модели, например:
45
+ # response = client.messages.create(messages=history, ...)
46
+ # return response.content[0].text
47
+ ...
48
+
49
+ result = connect("Посчитай <calculate 2+2>", my_generate)
50
+ ```
51
+
52
+ ## Поддерживаемые теги
53
+
54
+ | Тег | Что делает |
55
+ |---|---|
56
+ | `<calculate 2+2>` | Вычисляет выражение |
57
+ | `<date>`, `<date +1y>` | Текущая/сдвинутая дата |
58
+ | `<time>`, `<time +30m>` | Текущее/сдвинутое время |
59
+ | `<search запрос>` | Веб-поиск + краткая выжимка со страниц (нужен extra `search`) |
60
+ | `<fix_tags>` | Автоматически закрывает/выравнивает незакрытые HTML-теги в тексте до этого места |
61
+ | `<layout текст>` | Переключает раскладку текста RU⇄EN |
62
+
63
+ ## Кастомизация под свой ИИ
64
+
65
+ `connect` / `system` / `system_stream` работают из коробки со стандартным
66
+ форматом `{"role": ..., "content": ...}`, но принимают параметры для
67
+ любого другого формата сообщений, нестандартной сигнатуры функции
68
+ генерации или async-моделей. Подробности — в докстрингах функций.
69
+
70
+ ## Лицензия
71
+
72
+ MIT
@@ -0,0 +1,10 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/tools/__init__.py
5
+ src/tools/core.py
6
+ src/uvai_tools.egg-info/PKG-INFO
7
+ src/uvai_tools.egg-info/SOURCES.txt
8
+ src/uvai_tools.egg-info/dependency_links.txt
9
+ src/uvai_tools.egg-info/requires.txt
10
+ src/uvai_tools.egg-info/top_level.txt
@@ -0,0 +1,5 @@
1
+ python-dateutil>=2.8
2
+
3
+ [search]
4
+ ddgs>=9.0
5
+ beautifulsoup4>=4.12