llms-py 3.0.6__py3-none-any.whl → 3.0.8__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.
- llms/extensions/analytics/ui/index.mjs +1 -1
- llms/extensions/app/__init__.py +3 -1
- llms/extensions/app/ui/Recents.mjs +1 -1
- llms/extensions/app/ui/threadStore.mjs +4 -3
- llms/extensions/core_tools/__init__.py +14 -12
- llms/extensions/providers/__init__.py +2 -0
- llms/extensions/providers/anthropic.py +1 -1
- llms/extensions/providers/chutes.py +7 -9
- llms/extensions/providers/google.py +3 -3
- llms/extensions/providers/nvidia.py +9 -11
- llms/extensions/providers/openai.py +1 -3
- llms/extensions/providers/zai.py +182 -0
- llms/extensions/tools/__init__.py +140 -1
- llms/extensions/tools/ui/index.mjs +552 -50
- llms/llms.json +14 -2
- llms/main.py +461 -99
- llms/providers-extra.json +38 -0
- llms/providers.json +1 -1
- llms/ui/App.mjs +1 -1
- llms/ui/ai.mjs +1 -1
- llms/ui/app.css +287 -18
- llms/ui/ctx.mjs +16 -3
- llms/ui/index.mjs +1 -1
- llms/ui/modules/chat/ChatBody.mjs +384 -107
- llms/ui/modules/chat/index.mjs +18 -4
- llms/ui/tailwind.input.css +54 -0
- llms/ui/utils.mjs +33 -4
- {llms_py-3.0.6.dist-info → llms_py-3.0.8.dist-info}/METADATA +1 -1
- {llms_py-3.0.6.dist-info → llms_py-3.0.8.dist-info}/RECORD +33 -32
- {llms_py-3.0.6.dist-info → llms_py-3.0.8.dist-info}/WHEEL +0 -0
- {llms_py-3.0.6.dist-info → llms_py-3.0.8.dist-info}/entry_points.txt +0 -0
- {llms_py-3.0.6.dist-info → llms_py-3.0.8.dist-info}/licenses/LICENSE +0 -0
- {llms_py-3.0.6.dist-info → llms_py-3.0.8.dist-info}/top_level.txt +0 -0
|
@@ -370,7 +370,7 @@ export const Analytics = {
|
|
|
370
370
|
</div>
|
|
371
371
|
<div>
|
|
372
372
|
<div class="text-xs text-gray-500 dark:text-gray-400 font-medium">Duration</div>
|
|
373
|
-
<div v-if="request.duration" class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ $fmt.humanifyMs(request.duration) }}</div>
|
|
373
|
+
<div v-if="request.duration" class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ $fmt.humanifyMs(request.duration * 1000) }}</div>
|
|
374
374
|
</div>
|
|
375
375
|
<div>
|
|
376
376
|
<div class="text-xs text-gray-500 dark:text-gray-400 font-medium">Speed</div>
|
llms/extensions/app/__init__.py
CHANGED
|
@@ -444,7 +444,9 @@ def install(ctx):
|
|
|
444
444
|
input_tokens = usage.get("prompt_tokens", 0)
|
|
445
445
|
output_tokens = usage.get("completion_tokens", 0)
|
|
446
446
|
total_tokens = usage.get("total_tokens", input_tokens + output_tokens)
|
|
447
|
-
cost =
|
|
447
|
+
cost = usage.get("cost") or o.get(
|
|
448
|
+
"cost", ((input_price * input_tokens) + (output_price * output_tokens)) / 1000000
|
|
449
|
+
)
|
|
448
450
|
|
|
449
451
|
request = {
|
|
450
452
|
"user": user,
|
|
@@ -67,7 +67,7 @@ const RecentResults = {
|
|
|
67
67
|
|
|
68
68
|
const normalized = (s) => (s || '').toString().toLowerCase()
|
|
69
69
|
const replaceChars = new Set('<>`*|#'.split(''))
|
|
70
|
-
const clean = s => [...s].map(c => replaceChars.has(c) ? ' ' : c).join('')
|
|
70
|
+
const clean = s => [...(s || '')].map(c => replaceChars.has(c) ? ' ' : c).join('')
|
|
71
71
|
|
|
72
72
|
const loadMore = async (reset = false) => {
|
|
73
73
|
if (reset) {
|
|
@@ -139,10 +139,11 @@ async function updateThread(threadId, updates) {
|
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
async function deleteMessageFromThread(threadId,
|
|
142
|
+
async function deleteMessageFromThread(threadId, timestamp) {
|
|
143
143
|
const thread = await getThread(threadId)
|
|
144
144
|
if (!thread) throw new Error('Thread not found')
|
|
145
|
-
const updatedMessages = thread.messages.filter(m => m.
|
|
145
|
+
const updatedMessages = thread.messages.filter(m => m.timestamp !== timestamp)
|
|
146
|
+
console.log('deleteMessageFromThread', threadId, timestamp, updatedMessages)
|
|
146
147
|
await updateThread(threadId, { messages: updatedMessages })
|
|
147
148
|
}
|
|
148
149
|
|
|
@@ -150,7 +151,7 @@ async function updateMessageInThread(threadId, messageId, updates) {
|
|
|
150
151
|
const thread = await getThread(threadId)
|
|
151
152
|
if (!thread) throw new Error('Thread not found')
|
|
152
153
|
|
|
153
|
-
const messageIndex = thread.messages.findIndex(m => m.
|
|
154
|
+
const messageIndex = thread.messages.findIndex(m => m.timestamp === messageId)
|
|
154
155
|
if (messageIndex === -1) throw new Error('Message not found')
|
|
155
156
|
|
|
156
157
|
const updatedMessages = [...thread.messages]
|
|
@@ -538,12 +538,13 @@ def get_current_time(tz_name: Optional[str] = None) -> str:
|
|
|
538
538
|
def install(ctx):
|
|
539
539
|
global g_ctx
|
|
540
540
|
g_ctx = ctx
|
|
541
|
+
group = "core_tools"
|
|
541
542
|
# Examples of registering tools using automatic definition generation
|
|
542
|
-
ctx.register_tool(memory_read)
|
|
543
|
-
ctx.register_tool(memory_write)
|
|
543
|
+
ctx.register_tool(memory_read, group=group)
|
|
544
|
+
ctx.register_tool(memory_write, group=group)
|
|
544
545
|
# ctx.register_tool(semantic_search) # TODO: implement
|
|
545
|
-
ctx.register_tool(read_file)
|
|
546
|
-
ctx.register_tool(write_file)
|
|
546
|
+
ctx.register_tool(read_file, group=group)
|
|
547
|
+
ctx.register_tool(write_file, group=group)
|
|
547
548
|
ctx.register_tool(
|
|
548
549
|
edit_file,
|
|
549
550
|
{
|
|
@@ -562,15 +563,16 @@ def install(ctx):
|
|
|
562
563
|
},
|
|
563
564
|
},
|
|
564
565
|
},
|
|
566
|
+
group=group,
|
|
565
567
|
)
|
|
566
|
-
ctx.register_tool(list_directory)
|
|
567
|
-
ctx.register_tool(glob_paths)
|
|
568
|
-
ctx.register_tool(calc)
|
|
569
|
-
ctx.register_tool(run_python)
|
|
570
|
-
ctx.register_tool(run_typescript)
|
|
571
|
-
ctx.register_tool(run_javascript)
|
|
572
|
-
ctx.register_tool(run_csharp)
|
|
573
|
-
ctx.register_tool(get_current_time)
|
|
568
|
+
ctx.register_tool(list_directory, group=group)
|
|
569
|
+
ctx.register_tool(glob_paths, group=group)
|
|
570
|
+
ctx.register_tool(calc, group=group)
|
|
571
|
+
ctx.register_tool(run_python, group=group)
|
|
572
|
+
ctx.register_tool(run_typescript, group=group)
|
|
573
|
+
ctx.register_tool(run_javascript, group=group)
|
|
574
|
+
ctx.register_tool(run_csharp, group=group)
|
|
575
|
+
ctx.register_tool(get_current_time, group=group)
|
|
574
576
|
|
|
575
577
|
def exec_language(language: str, code: str) -> Dict[str, Any]:
|
|
576
578
|
if language == "python":
|
|
@@ -5,6 +5,7 @@ from .google import install_google
|
|
|
5
5
|
from .nvidia import install_nvidia
|
|
6
6
|
from .openai import install_openai
|
|
7
7
|
from .openrouter import install_openrouter
|
|
8
|
+
from .zai import install_zai
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
def install(ctx):
|
|
@@ -15,6 +16,7 @@ def install(ctx):
|
|
|
15
16
|
install_nvidia(ctx)
|
|
16
17
|
install_openai(ctx)
|
|
17
18
|
install_openrouter(ctx)
|
|
19
|
+
install_zai(ctx)
|
|
18
20
|
|
|
19
21
|
|
|
20
22
|
__install__ = install
|
|
@@ -221,7 +221,7 @@ def install_anthropic(ctx):
|
|
|
221
221
|
# Add metadata
|
|
222
222
|
if "metadata" not in ret:
|
|
223
223
|
ret["metadata"] = {}
|
|
224
|
-
ret["metadata"]["duration"] = int(
|
|
224
|
+
ret["metadata"]["duration"] = int(time.time() - started_at)
|
|
225
225
|
|
|
226
226
|
if chat is not None and "model" in chat:
|
|
227
227
|
cost = self.model_cost(chat["model"])
|
|
@@ -66,15 +66,13 @@ def install_chutes(ctx):
|
|
|
66
66
|
if chat["model"] in self.model_negative_prompt:
|
|
67
67
|
payload["negative_prompt"] = self.negative_prompt
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
payload["width"] = width
|
|
77
|
-
payload["height"] = height
|
|
69
|
+
aspect_ratio = ctx.chat_to_aspect_ratio(chat) or "1:1"
|
|
70
|
+
dimension = ctx.app.aspect_ratios.get(aspect_ratio)
|
|
71
|
+
if dimension:
|
|
72
|
+
w, h = dimension.split("×")
|
|
73
|
+
width, height = int(w), int(h)
|
|
74
|
+
payload["width"] = width
|
|
75
|
+
payload["height"] = height
|
|
78
76
|
|
|
79
77
|
if chat["model"] in self.model_resolutions:
|
|
80
78
|
# if models use resolution, remove width and height
|
|
@@ -471,9 +471,9 @@ def install_google(ctx):
|
|
|
471
471
|
if "usageMetadata" in obj:
|
|
472
472
|
usage = obj["usageMetadata"]
|
|
473
473
|
response["usage"] = {
|
|
474
|
-
"completion_tokens": usage
|
|
475
|
-
"total_tokens": usage
|
|
476
|
-
"prompt_tokens": usage
|
|
474
|
+
"completion_tokens": usage.get("candidatesTokenCount", 0),
|
|
475
|
+
"total_tokens": usage.get("totalTokenCount", 0),
|
|
476
|
+
"prompt_tokens": usage.get("promptTokenCount", 0),
|
|
477
477
|
}
|
|
478
478
|
|
|
479
479
|
return ctx.log_json(self.to_response(response, chat, started_at))
|
|
@@ -66,17 +66,15 @@ def install_nvidia(ctx):
|
|
|
66
66
|
}
|
|
67
67
|
modalities = chat.get("modalities", ["text"])
|
|
68
68
|
if "image" in modalities:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
gen_request["width"] = self.width
|
|
79
|
-
gen_request["height"] = self.height
|
|
69
|
+
aspect_ratio = ctx.chat_to_aspect_ratio(chat) or "1:1"
|
|
70
|
+
dimension = ctx.app.aspect_ratios.get(aspect_ratio)
|
|
71
|
+
if dimension:
|
|
72
|
+
width, height = dimension.split("×")
|
|
73
|
+
gen_request["width"] = int(width)
|
|
74
|
+
gen_request["height"] = int(height)
|
|
75
|
+
else:
|
|
76
|
+
gen_request["width"] = self.width
|
|
77
|
+
gen_request["height"] = self.height
|
|
80
78
|
|
|
81
79
|
gen_request["mode"] = self.mode
|
|
82
80
|
gen_request["cfg_scale"] = self.cfg_scale
|
|
@@ -119,9 +119,7 @@ def install_openai(ctx):
|
|
|
119
119
|
if chat["model"] in self.map_image_models:
|
|
120
120
|
chat["model"] = self.map_image_models[chat["model"]]
|
|
121
121
|
|
|
122
|
-
aspect_ratio = "1:1"
|
|
123
|
-
if "image_config" in chat and "aspect_ratio" in chat["image_config"]:
|
|
124
|
-
aspect_ratio = chat["image_config"].get("aspect_ratio", "1:1")
|
|
122
|
+
aspect_ratio = ctx.chat_to_aspect_ratio(chat) or "1:1"
|
|
125
123
|
payload = {
|
|
126
124
|
"model": chat["model"],
|
|
127
125
|
"prompt": ctx.last_user_prompt(chat),
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import time
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import aiohttp
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def install_zai(ctx):
|
|
9
|
+
from llms.main import GeneratorBase
|
|
10
|
+
|
|
11
|
+
# https://docs.z.ai/guides/image/glm-image
|
|
12
|
+
class ZaiGenerator(GeneratorBase):
|
|
13
|
+
sdk = "zai/image"
|
|
14
|
+
|
|
15
|
+
def __init__(self, **kwargs):
|
|
16
|
+
super().__init__(**kwargs)
|
|
17
|
+
self.aspect_ratios = {
|
|
18
|
+
"1:1": "1280×1280",
|
|
19
|
+
"2:3": "1056×1568",
|
|
20
|
+
"3:2": "1568×1056",
|
|
21
|
+
"3:4": "1088×1472",
|
|
22
|
+
"4:3": "1472×1088",
|
|
23
|
+
"4:5": "1088×1472",
|
|
24
|
+
"5:4": "1472×1088",
|
|
25
|
+
"9:16": "960×1728",
|
|
26
|
+
"16:9": "1728×960",
|
|
27
|
+
"21:9": "1728×960",
|
|
28
|
+
}
|
|
29
|
+
self.model: str = kwargs.get("model", "glm-image")
|
|
30
|
+
self.n: Optional[int] = kwargs.get("n")
|
|
31
|
+
self.quality: Optional[str] = kwargs.get("quality")
|
|
32
|
+
self.response_format: Optional[str] = kwargs.get("response_format")
|
|
33
|
+
self.size: Optional[str] = kwargs.get("size")
|
|
34
|
+
self.style: Optional[str] = kwargs.get("style")
|
|
35
|
+
self.sensitive_word_check: Optional[str] = kwargs.get("sensitive_word_check")
|
|
36
|
+
self.user: Optional[str] = kwargs.get("user")
|
|
37
|
+
self.request_id: Optional[str] = kwargs.get("request_id")
|
|
38
|
+
self.user_id: Optional[str] = kwargs.get("user_id")
|
|
39
|
+
self.extra_headers: Optional[dict] = kwargs.get("extra_headers")
|
|
40
|
+
self.extra_body: Optional[dict] = kwargs.get("extra_body")
|
|
41
|
+
self.disable_strict_validation: Optional[bool] = kwargs.get("disable_strict_validation")
|
|
42
|
+
self.timeout: Optional[float] = float(kwargs.get("timeout") or 300)
|
|
43
|
+
self.watermark_enabled: Optional[bool] = kwargs.get("watermark_enabled")
|
|
44
|
+
|
|
45
|
+
async def chat(self, chat, provider=None, context=None):
|
|
46
|
+
headers = {"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"}
|
|
47
|
+
if self.extra_headers:
|
|
48
|
+
headers.update(self.extra_headers)
|
|
49
|
+
|
|
50
|
+
chat_url = "https://api.z.ai/api/paas/v4/images/generations"
|
|
51
|
+
if provider is not None:
|
|
52
|
+
headers["Authorization"] = f"Bearer {provider.api_key}"
|
|
53
|
+
chat["model"] = provider.provider_model(chat["model"]) or chat["model"]
|
|
54
|
+
chat_url = provider.api + "/images/generations"
|
|
55
|
+
|
|
56
|
+
body = {}
|
|
57
|
+
attrs = [
|
|
58
|
+
"model",
|
|
59
|
+
"n",
|
|
60
|
+
"quality",
|
|
61
|
+
"response_format",
|
|
62
|
+
"size",
|
|
63
|
+
"style",
|
|
64
|
+
"sensitive_word_check",
|
|
65
|
+
"user",
|
|
66
|
+
"request_id",
|
|
67
|
+
"user_id",
|
|
68
|
+
"disable_strict_validation",
|
|
69
|
+
"watermark_enabled",
|
|
70
|
+
]
|
|
71
|
+
for attr in attrs:
|
|
72
|
+
if hasattr(self, attr) and getattr(self, attr) is not None:
|
|
73
|
+
body[attr] = getattr(self, attr)
|
|
74
|
+
|
|
75
|
+
if self.extra_body:
|
|
76
|
+
body.update(self.extra_body)
|
|
77
|
+
|
|
78
|
+
if "model" in chat:
|
|
79
|
+
body["model"] = chat["model"]
|
|
80
|
+
|
|
81
|
+
body["prompt"] = ctx.last_user_prompt(chat)
|
|
82
|
+
|
|
83
|
+
aspect_ratio = ctx.chat_to_aspect_ratio(chat) or "1:1"
|
|
84
|
+
size = self.aspect_ratios.get(aspect_ratio, "1280x1280").replace("×", "x")
|
|
85
|
+
body["size"] = size
|
|
86
|
+
|
|
87
|
+
username = ctx.context_to_username(context)
|
|
88
|
+
if username:
|
|
89
|
+
body["user"] = username
|
|
90
|
+
|
|
91
|
+
ctx.dbg(f"ZaiProvider.chat: {chat_url}")
|
|
92
|
+
ctx.dbg(json.dumps(body, indent=2))
|
|
93
|
+
started_at = time.time()
|
|
94
|
+
async with aiohttp.ClientSession() as session, session.post(
|
|
95
|
+
chat_url,
|
|
96
|
+
headers=headers,
|
|
97
|
+
data=json.dumps(body),
|
|
98
|
+
timeout=aiohttp.ClientTimeout(total=self.timeout),
|
|
99
|
+
) as response:
|
|
100
|
+
# Example Response
|
|
101
|
+
# {
|
|
102
|
+
# "created": 1768451303,
|
|
103
|
+
# "data": [
|
|
104
|
+
# {
|
|
105
|
+
# "url": "https://mfile.z.ai/1768451374203-b334959408a643a8a6c74eb104746dcb.png?ufileattname=202601151228236805d575507d4570_watermark.png"
|
|
106
|
+
# }
|
|
107
|
+
# ],
|
|
108
|
+
# "id": "202601151228236805d575507d4570",
|
|
109
|
+
# "request_id": "202601151228236805d575507d4570",
|
|
110
|
+
# "usage": {
|
|
111
|
+
# "tokens": 0,
|
|
112
|
+
# "price": 0,
|
|
113
|
+
# "cost": 0.0,
|
|
114
|
+
# "duration": 71
|
|
115
|
+
# },
|
|
116
|
+
# "timestamp": 1768451374519,
|
|
117
|
+
# "model": "GLM-Image"
|
|
118
|
+
# }
|
|
119
|
+
|
|
120
|
+
response_json = await self.response_json(response)
|
|
121
|
+
duration = int(time.time() - started_at)
|
|
122
|
+
usage = response_json.get("usage", {})
|
|
123
|
+
if context is not None:
|
|
124
|
+
context["providerResponse"] = response_json
|
|
125
|
+
if "cost" in usage:
|
|
126
|
+
context["cost"] = usage.get("cost")
|
|
127
|
+
|
|
128
|
+
images = []
|
|
129
|
+
for image in response_json.get("data", []):
|
|
130
|
+
url = image.get("url")
|
|
131
|
+
if not url:
|
|
132
|
+
continue
|
|
133
|
+
# download url with aiohttp
|
|
134
|
+
async with session.get(url) as image_response:
|
|
135
|
+
headers = image_response.headers
|
|
136
|
+
# get filename from Content-Disposition
|
|
137
|
+
# attachment; filename="202601151228236805d575507d4570_watermark.png"
|
|
138
|
+
mime_type = headers.get("Content-Type") or "image/png"
|
|
139
|
+
disposition = headers.get("Content-Disposition")
|
|
140
|
+
if disposition:
|
|
141
|
+
start = disposition.index('filename="') + len('filename="')
|
|
142
|
+
end = disposition.index('"', start)
|
|
143
|
+
filename = disposition[start:end]
|
|
144
|
+
else:
|
|
145
|
+
ext = mime_type.split("/")[1]
|
|
146
|
+
filename = f"{body['model'].lower()}-{response_json.get('id', int(started_at))}.{ext}"
|
|
147
|
+
image_bytes = await image_response.read()
|
|
148
|
+
|
|
149
|
+
info = {
|
|
150
|
+
"prompt": body["prompt"],
|
|
151
|
+
"type": mime_type,
|
|
152
|
+
"width": int(size.split("x")[0]),
|
|
153
|
+
"height": int(size.split("x")[1]),
|
|
154
|
+
"duration": duration,
|
|
155
|
+
}
|
|
156
|
+
info.update(usage)
|
|
157
|
+
cache_url, info = ctx.save_image_to_cache(
|
|
158
|
+
image_bytes, filename, image_info=info, ignore_info=True
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
images.append(
|
|
162
|
+
{
|
|
163
|
+
"type": "image_url",
|
|
164
|
+
"image_url": {
|
|
165
|
+
"url": cache_url,
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
chat_response = {
|
|
171
|
+
"choices": [{"message": {"role": "assistant", "content": self.default_content, "images": images}}],
|
|
172
|
+
"created": int(time.time()),
|
|
173
|
+
"usage": {
|
|
174
|
+
"prompt_tokens": 0,
|
|
175
|
+
"completion_tokens": 1_000_000, # Price per image is 0.015, so 1M token is 0.015
|
|
176
|
+
},
|
|
177
|
+
}
|
|
178
|
+
if "cost" in usage:
|
|
179
|
+
chat_response["cost"] = usage["cost"]
|
|
180
|
+
return ctx.log_json(chat_response)
|
|
181
|
+
|
|
182
|
+
ctx.add_provider(ZaiGenerator)
|
|
@@ -1,5 +1,144 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
1
3
|
from aiohttp import web
|
|
2
4
|
|
|
3
5
|
|
|
4
6
|
def install(ctx):
|
|
5
|
-
|
|
7
|
+
async def tools_handler(request):
|
|
8
|
+
return web.json_response(
|
|
9
|
+
{
|
|
10
|
+
"groups": ctx.app.tool_groups,
|
|
11
|
+
"definitions": ctx.app.tool_definitions,
|
|
12
|
+
}
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
ctx.add_get("", tools_handler)
|
|
16
|
+
|
|
17
|
+
def prop_def_types(prop_def):
|
|
18
|
+
prop_type = prop_def.get("type")
|
|
19
|
+
if not prop_type:
|
|
20
|
+
any_of = prop_def.get("anyOf")
|
|
21
|
+
if any_of:
|
|
22
|
+
return [item.get("type") for item in any_of]
|
|
23
|
+
else:
|
|
24
|
+
return []
|
|
25
|
+
return [prop_type]
|
|
26
|
+
|
|
27
|
+
def tool_prop_value(value, prop_def):
|
|
28
|
+
"""
|
|
29
|
+
Convert a value to the specified type.
|
|
30
|
+
types: string, number, integer, boolean, object, array, null
|
|
31
|
+
example prop_def = [
|
|
32
|
+
{
|
|
33
|
+
"type": "string"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"default": "name",
|
|
37
|
+
"type": "string",
|
|
38
|
+
"enum": ["name", "size"]
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"default": [],
|
|
42
|
+
"type": "array",
|
|
43
|
+
"items": {
|
|
44
|
+
"type": "string"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"anyOf": [
|
|
49
|
+
{
|
|
50
|
+
"type": "string"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"type": "null"
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
"default": null,
|
|
57
|
+
},
|
|
58
|
+
]
|
|
59
|
+
"""
|
|
60
|
+
if value is None:
|
|
61
|
+
default = prop_def.get("default")
|
|
62
|
+
if default is not None:
|
|
63
|
+
default = tool_prop_value(default, prop_def)
|
|
64
|
+
return default
|
|
65
|
+
|
|
66
|
+
prop_types = prop_def_types(prop_def)
|
|
67
|
+
if "integer" in prop_types:
|
|
68
|
+
return int(value)
|
|
69
|
+
elif "number" in prop_types:
|
|
70
|
+
return float(value)
|
|
71
|
+
elif "boolean" in prop_types:
|
|
72
|
+
return bool(value)
|
|
73
|
+
elif "object" in prop_types:
|
|
74
|
+
return value if isinstance(value, dict) else json.loads(value)
|
|
75
|
+
elif "array" in prop_types:
|
|
76
|
+
return value if isinstance(value, list) else value.split(",")
|
|
77
|
+
else:
|
|
78
|
+
enum = prop_def.get("enum")
|
|
79
|
+
if enum and value not in enum:
|
|
80
|
+
raise Exception(f"'{value}' is not in {enum}")
|
|
81
|
+
return value
|
|
82
|
+
|
|
83
|
+
async def exec_handler(request):
|
|
84
|
+
name = request.match_info.get("name")
|
|
85
|
+
args = await request.json()
|
|
86
|
+
|
|
87
|
+
tool_def = ctx.get_tool_definition(name)
|
|
88
|
+
if not tool_def:
|
|
89
|
+
raise Exception(f"Tool '{name}' not found")
|
|
90
|
+
|
|
91
|
+
type = tool_def.get("type")
|
|
92
|
+
if type != "function":
|
|
93
|
+
raise Exception(f"Tool '{name}' of type '{type}' is not supported")
|
|
94
|
+
|
|
95
|
+
ctx.dbg(f"Executing tool '{name}' with args:\n{json.dumps(args, indent=2)}")
|
|
96
|
+
function_args = {}
|
|
97
|
+
parameters = tool_def.get("function", {}).get("parameters")
|
|
98
|
+
if parameters:
|
|
99
|
+
properties = parameters.get("properties")
|
|
100
|
+
required_props = parameters.get("required", [])
|
|
101
|
+
if properties:
|
|
102
|
+
for prop_name, prop_def in properties.items():
|
|
103
|
+
prop_title = prop_def.get("title", prop_name)
|
|
104
|
+
prop_types = prop_def_types(prop_def)
|
|
105
|
+
value = None
|
|
106
|
+
if prop_name in args:
|
|
107
|
+
value = tool_prop_value(args[prop_name], prop_def)
|
|
108
|
+
elif prop_name in required_props:
|
|
109
|
+
if "null" in prop_types:
|
|
110
|
+
value = None
|
|
111
|
+
elif "default" in prop_def:
|
|
112
|
+
value = tool_prop_value(prop_def["default"], prop_def)
|
|
113
|
+
else:
|
|
114
|
+
raise Exception(f"Missing required parameter '{prop_title}' for tool '{name}'")
|
|
115
|
+
if value is not None or "null" in prop_types:
|
|
116
|
+
function_args[prop_name] = value
|
|
117
|
+
else:
|
|
118
|
+
ctx.dbg(f"tool '{name}' has no properties:\n{json.dumps(tool_def, indent=2)}")
|
|
119
|
+
else:
|
|
120
|
+
ctx.dbg(f"tool '{name}' has no parameters:\n{json.dumps(tool_def, indent=2)}")
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
text, resources = await ctx.exec_tool(name, function_args)
|
|
124
|
+
|
|
125
|
+
results = []
|
|
126
|
+
if text:
|
|
127
|
+
results.append(
|
|
128
|
+
{
|
|
129
|
+
"type": "text",
|
|
130
|
+
"text": text,
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
if resources:
|
|
134
|
+
results.extend(resources)
|
|
135
|
+
|
|
136
|
+
return web.json_response(results)
|
|
137
|
+
except Exception as e:
|
|
138
|
+
ctx.err(f"Failed to execute tool '{name}' with args:\n{json.dumps(function_args, indent=2)}", e)
|
|
139
|
+
raise e
|
|
140
|
+
|
|
141
|
+
ctx.add_post("exec/{name}", exec_handler)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
__install__ = install
|