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.
- uvai_tools-0.1.0/LICENSE +21 -0
- uvai_tools-0.1.0/PKG-INFO +72 -0
- uvai_tools-0.1.0/README.md +52 -0
- uvai_tools-0.1.0/pyproject.toml +34 -0
- uvai_tools-0.1.0/setup.cfg +4 -0
- uvai_tools-0.1.0/src/tools/__init__.py +41 -0
- uvai_tools-0.1.0/src/tools/core.py +478 -0
- uvai_tools-0.1.0/src/uvai_tools.egg-info/PKG-INFO +72 -0
- uvai_tools-0.1.0/src/uvai_tools.egg-info/SOURCES.txt +10 -0
- uvai_tools-0.1.0/src/uvai_tools.egg-info/dependency_links.txt +1 -0
- uvai_tools-0.1.0/src/uvai_tools.egg-info/requires.txt +5 -0
- uvai_tools-0.1.0/src/uvai_tools.egg-info/top_level.txt +1 -0
uvai_tools-0.1.0/LICENSE
ADDED
|
@@ -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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tools
|