llms-py 3.0.15__py3-none-any.whl → 3.0.17__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/app/__init__.py +0 -1
- llms/extensions/app/db.py +5 -1
- llms/extensions/computer/__init__.py +59 -0
- llms/extensions/{computer_use → computer}/bash.py +2 -2
- llms/extensions/{computer_use → computer}/edit.py +10 -14
- llms/extensions/computer/filesystem.py +542 -0
- llms/extensions/core_tools/__init__.py +0 -38
- llms/extensions/providers/cerebras.py +0 -1
- llms/extensions/providers/google.py +57 -30
- llms/extensions/skills/ui/index.mjs +27 -0
- llms/extensions/tools/__init__.py +5 -82
- llms/extensions/tools/ui/index.mjs +92 -4
- llms/main.py +225 -34
- llms/ui/ai.mjs +1 -1
- llms/ui/app.css +491 -0
- llms/ui/modules/chat/ChatBody.mjs +64 -9
- llms/ui/modules/chat/index.mjs +103 -91
- {llms_py-3.0.15.dist-info → llms_py-3.0.17.dist-info}/METADATA +1 -1
- {llms_py-3.0.15.dist-info → llms_py-3.0.17.dist-info}/RECORD +28 -27
- {llms_py-3.0.15.dist-info → llms_py-3.0.17.dist-info}/WHEEL +1 -1
- llms/extensions/computer_use/__init__.py +0 -27
- /llms/extensions/{computer_use → computer}/README.md +0 -0
- /llms/extensions/{computer_use → computer}/base.py +0 -0
- /llms/extensions/{computer_use → computer}/computer.py +0 -0
- /llms/extensions/{computer_use → computer}/platform.py +0 -0
- /llms/extensions/{computer_use → computer}/run.py +0 -0
- {llms_py-3.0.15.dist-info → llms_py-3.0.17.dist-info}/entry_points.txt +0 -0
- {llms_py-3.0.15.dist-info → llms_py-3.0.17.dist-info}/licenses/LICENSE +0 -0
- {llms_py-3.0.15.dist-info → llms_py-3.0.17.dist-info}/top_level.txt +0 -0
|
@@ -102,24 +102,6 @@ def write_file(path: str, content: str) -> bool:
|
|
|
102
102
|
return True
|
|
103
103
|
|
|
104
104
|
|
|
105
|
-
def edit_file(path: str, old_str: str, new_str: str) -> Dict[str, Any]:
|
|
106
|
-
"""
|
|
107
|
-
Replaces first occurrence of old_str with new_str in file. If old_str is empty,
|
|
108
|
-
create/overwrite file with new_str.
|
|
109
|
-
:return: A dictionary with the path to the file and the action taken.
|
|
110
|
-
"""
|
|
111
|
-
safe_path = _resolve_safe_path(path)
|
|
112
|
-
if old_str == "":
|
|
113
|
-
safe_path.write_text(new_str, encoding="utf-8")
|
|
114
|
-
return {"path": str(safe_path), "action": "created_file"}
|
|
115
|
-
original = safe_path.read_text(encoding="utf-8")
|
|
116
|
-
if original.find(old_str) == -1:
|
|
117
|
-
return {"path": str(safe_path), "action": "old_str not found"}
|
|
118
|
-
edited = original.replace(old_str, new_str, 1)
|
|
119
|
-
safe_path.write_text(edited, encoding="utf-8")
|
|
120
|
-
return {"path": str(safe_path), "action": "edited"}
|
|
121
|
-
|
|
122
|
-
|
|
123
105
|
def list_directory(path: str) -> str:
|
|
124
106
|
"""List directory contents"""
|
|
125
107
|
safe_path = _resolve_safe_path(path)
|
|
@@ -545,26 +527,6 @@ def install(ctx):
|
|
|
545
527
|
# ctx.register_tool(semantic_search) # TODO: implement
|
|
546
528
|
ctx.register_tool(read_file, group=group)
|
|
547
529
|
ctx.register_tool(write_file, group=group)
|
|
548
|
-
ctx.register_tool(
|
|
549
|
-
edit_file,
|
|
550
|
-
{
|
|
551
|
-
"type": "function",
|
|
552
|
-
"function": {
|
|
553
|
-
"name": "edit_file",
|
|
554
|
-
"description": "Replaces first occurrence of old_str with new_str in file. If old_str is empty, create/overwrite file with new_str.",
|
|
555
|
-
"parameters": {
|
|
556
|
-
"type": "object",
|
|
557
|
-
"properties": {
|
|
558
|
-
"path": {"type": "string", "description": "Path to the file to edit."},
|
|
559
|
-
"old_str": {"type": "string", "description": "String to replace."},
|
|
560
|
-
"new_str": {"type": "string", "description": "String to replace with."},
|
|
561
|
-
},
|
|
562
|
-
"required": ["path", "old_str", "new_str"],
|
|
563
|
-
},
|
|
564
|
-
},
|
|
565
|
-
},
|
|
566
|
-
group=group,
|
|
567
|
-
)
|
|
568
530
|
ctx.register_tool(list_directory, group=group)
|
|
569
531
|
ctx.register_tool(glob_paths, group=group)
|
|
570
532
|
ctx.register_tool(calc, group=group)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
import base64
|
|
2
3
|
import io
|
|
3
4
|
import json
|
|
@@ -360,36 +361,62 @@ def install_google(ctx):
|
|
|
360
361
|
ctx.log(gemini_chat_summary(gemini_chat))
|
|
361
362
|
started_at = time.time()
|
|
362
363
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
headers=self.headers,
|
|
372
|
-
data=json.dumps(gemini_chat),
|
|
373
|
-
timeout=aiohttp.ClientTimeout(total=120),
|
|
374
|
-
) as res:
|
|
375
|
-
obj = await self.response_json(res)
|
|
376
|
-
if context is not None:
|
|
377
|
-
context["providerResponse"] = obj
|
|
378
|
-
except Exception as e:
|
|
379
|
-
ctx.log(f"Error: {res.status} {res.reason}: {e}")
|
|
380
|
-
text = await res.text()
|
|
364
|
+
max_retries = 3
|
|
365
|
+
for attempt in range(max_retries):
|
|
366
|
+
if ctx.MOCK and "modalities" in chat:
|
|
367
|
+
print("Mocking Google Gemini Image")
|
|
368
|
+
with open(f"{ctx.MOCK_DIR}/gemini-image.json") as f:
|
|
369
|
+
obj = json.load(f)
|
|
370
|
+
else:
|
|
371
|
+
res = None
|
|
381
372
|
try:
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
373
|
+
if attempt > 0:
|
|
374
|
+
await asyncio.sleep(attempt * 0.5)
|
|
375
|
+
ctx.log(f"Retrying request (attempt {attempt + 1}/{max_retries})...")
|
|
376
|
+
|
|
377
|
+
async with session.post(
|
|
378
|
+
gemini_chat_url,
|
|
379
|
+
headers=self.headers,
|
|
380
|
+
data=json.dumps(gemini_chat),
|
|
381
|
+
timeout=aiohttp.ClientTimeout(total=120),
|
|
382
|
+
) as res:
|
|
383
|
+
obj = await self.response_json(res)
|
|
384
|
+
if context is not None:
|
|
385
|
+
context["providerResponse"] = obj
|
|
386
|
+
except Exception as e:
|
|
387
|
+
if res:
|
|
388
|
+
ctx.err(f"{res.status} {res.reason}", e)
|
|
389
|
+
try:
|
|
390
|
+
text = await res.text()
|
|
391
|
+
obj = json.loads(text)
|
|
392
|
+
except Exception as parseEx:
|
|
393
|
+
ctx.err("Failed to parse error response:\n" + text, parseEx)
|
|
394
|
+
raise e from None
|
|
395
|
+
else:
|
|
396
|
+
ctx.err(f"Request failed: {str(e)}")
|
|
397
|
+
raise e from None
|
|
398
|
+
|
|
399
|
+
if "error" in obj:
|
|
400
|
+
ctx.log(f"Error: {obj['error']}")
|
|
401
|
+
raise Exception(obj["error"]["message"])
|
|
402
|
+
|
|
403
|
+
if ctx.debug:
|
|
404
|
+
ctx.dbg(json.dumps(gemini_response_summary(obj), indent=2))
|
|
405
|
+
|
|
406
|
+
# Check for empty response "anomaly"
|
|
407
|
+
has_candidates = obj.get("candidates") and len(obj["candidates"]) > 0
|
|
408
|
+
if has_candidates:
|
|
409
|
+
candidate = obj["candidates"][0]
|
|
410
|
+
raw_content = candidate.get("content", {})
|
|
411
|
+
raw_parts = raw_content.get("parts", [])
|
|
412
|
+
|
|
413
|
+
if not raw_parts and attempt < max_retries - 1:
|
|
414
|
+
# It's an empty response candidates list
|
|
415
|
+
ctx.dbg("Empty candidates parts detected. Retrying...")
|
|
416
|
+
continue
|
|
417
|
+
|
|
418
|
+
# If we got here, it's either a good response or we ran out of retries
|
|
419
|
+
break
|
|
393
420
|
|
|
394
421
|
# calculate cost per generation
|
|
395
422
|
cost = None
|
|
@@ -504,7 +531,7 @@ def install_google(ctx):
|
|
|
504
531
|
"finish_reason": candidate.get("finishReason", "stop"),
|
|
505
532
|
"message": {
|
|
506
533
|
"role": role,
|
|
507
|
-
"content": content if content else
|
|
534
|
+
"content": content if content else "",
|
|
508
535
|
},
|
|
509
536
|
}
|
|
510
537
|
if reasoning:
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ref, inject, computed } from "vue"
|
|
2
|
+
import { leftPart } from "@servicestack/client"
|
|
2
3
|
|
|
3
4
|
let ext
|
|
4
5
|
|
|
@@ -319,6 +320,32 @@ export default {
|
|
|
319
320
|
context.requiredSystemPrompts.push(skillsPrompt)
|
|
320
321
|
})
|
|
321
322
|
|
|
323
|
+
ctx.setThreadFooters({
|
|
324
|
+
skills: {
|
|
325
|
+
component: {
|
|
326
|
+
template: `
|
|
327
|
+
<div class="mt-2 w-full flex justify-center">
|
|
328
|
+
<button type="button" @click="$ctx.chat.sendUserMessage('proceed')"
|
|
329
|
+
class="px-3 py-1 rounded-md text-xs font-medium border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors select-none">
|
|
330
|
+
proceed
|
|
331
|
+
</button>
|
|
332
|
+
</div>
|
|
333
|
+
`
|
|
334
|
+
},
|
|
335
|
+
show({ thread }) {
|
|
336
|
+
if (thread.messages.length < 2) return false
|
|
337
|
+
const msgRoles = thread.messages.map(m => m.role)
|
|
338
|
+
if (msgRoles[msgRoles.length - 1] != "assistant") return false
|
|
339
|
+
const hasSkillToolCall = thread.messages.some(m =>
|
|
340
|
+
m.tool_calls?.some(tc => tc.type == "function" && tc.function.name == "skill"))
|
|
341
|
+
const systemPrompt = thread.messages.find(m => m.role == "system")?.content.toLowerCase() || ''
|
|
342
|
+
const line1 = leftPart(systemPrompt.trim(), "\n")
|
|
343
|
+
const hasPlanSystemPrompt = line1.includes("plan") || systemPrompt.includes("# plan")
|
|
344
|
+
return hasSkillToolCall || hasPlanSystemPrompt
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
})
|
|
348
|
+
|
|
322
349
|
ctx.setState({
|
|
323
350
|
skills: {}
|
|
324
351
|
})
|
|
@@ -14,72 +14,6 @@ def install(ctx):
|
|
|
14
14
|
|
|
15
15
|
ctx.add_get("", tools_handler)
|
|
16
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
17
|
async def exec_handler(request):
|
|
84
18
|
name = request.match_info.get("name")
|
|
85
19
|
args = await request.json()
|
|
@@ -93,27 +27,16 @@ def install(ctx):
|
|
|
93
27
|
raise Exception(f"Tool '{name}' of type '{type}' is not supported")
|
|
94
28
|
|
|
95
29
|
ctx.dbg(f"Executing tool '{name}' with args:\n{json.dumps(args, indent=2)}")
|
|
30
|
+
|
|
31
|
+
# Filter args against tool properties
|
|
96
32
|
function_args = {}
|
|
97
33
|
parameters = tool_def.get("function", {}).get("parameters")
|
|
98
34
|
if parameters:
|
|
99
35
|
properties = parameters.get("properties")
|
|
100
|
-
required_props = parameters.get("required", [])
|
|
101
36
|
if properties:
|
|
102
|
-
for
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
37
|
+
for key in args:
|
|
38
|
+
if key in properties:
|
|
39
|
+
function_args[key] = args[key]
|
|
117
40
|
else:
|
|
118
41
|
ctx.dbg(f"tool '{name}' has no properties:\n{json.dumps(tool_def, indent=2)}")
|
|
119
42
|
else:
|
|
@@ -21,7 +21,7 @@ const ToolResult = {
|
|
|
21
21
|
<div class="not-prose py-2">
|
|
22
22
|
<pre v-if="ext.prefs.toolFormat !== 'preview'" class="tool-output">{{ origResult }}</pre>
|
|
23
23
|
<div v-else>
|
|
24
|
-
<ViewTypes v-if="Array.isArray(result)" :results="result" />
|
|
24
|
+
<ViewTypes v-if="Array.isArray(result) && result[0]?.type" :results="result" />
|
|
25
25
|
<ViewType v-else :result="result" />
|
|
26
26
|
</div>
|
|
27
27
|
</div>
|
|
@@ -76,10 +76,92 @@ const ToolResult = {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
const JsonInput = {
|
|
80
|
+
template: `
|
|
81
|
+
<div class="flex flex-col gap-1">
|
|
82
|
+
<div class="relative">
|
|
83
|
+
<textarea
|
|
84
|
+
v-model="localJson"
|
|
85
|
+
@input="validate"
|
|
86
|
+
rows="5"
|
|
87
|
+
class="w-full p-2 font-mono text-xs border rounded-md resize-y focus:outline-none focus:ring-2 transition-colors"
|
|
88
|
+
:class="error
|
|
89
|
+
? 'border-red-300 dark:border-red-700 bg-red-50 dark:bg-red-900/10 focus:ring-red-500'
|
|
90
|
+
: 'border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 focus:ring-blue-500'"
|
|
91
|
+
spellcheck="false"
|
|
92
|
+
></textarea>
|
|
93
|
+
<div v-if="isValid" class="absolute bottom-2 right-2 text-green-500 bg-white dark:bg-gray-800 rounded-full p-1 shadow-sm">
|
|
94
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
|
95
|
+
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
|
|
96
|
+
</svg>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
<div v-if="error" class="text-xs text-red-600 dark:text-red-400 font-medium px-1">
|
|
100
|
+
{{ error }}
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
`,
|
|
104
|
+
props: {
|
|
105
|
+
modelValue: {
|
|
106
|
+
required: true
|
|
107
|
+
}
|
|
82
108
|
},
|
|
109
|
+
emits: ['update:modelValue'],
|
|
110
|
+
setup(props, { emit }) {
|
|
111
|
+
// Initialize with formatted JSON
|
|
112
|
+
const localJson = ref(
|
|
113
|
+
props.modelValue !== undefined
|
|
114
|
+
? JSON.stringify(props.modelValue, null, 4)
|
|
115
|
+
: ''
|
|
116
|
+
)
|
|
117
|
+
const error = ref(null)
|
|
118
|
+
const isValid = ref(true)
|
|
119
|
+
|
|
120
|
+
function validate() {
|
|
121
|
+
try {
|
|
122
|
+
if (!localJson.value.trim()) {
|
|
123
|
+
// Decide if empty string is valid object/array or undefined
|
|
124
|
+
// For now, let's say empty is NOT valid if the prop expects object
|
|
125
|
+
// But maybe we can treat it as valid undefined/null?
|
|
126
|
+
// Let's enforce valid JSON.
|
|
127
|
+
if (localJson.value === '') {
|
|
128
|
+
error.value = null
|
|
129
|
+
isValid.value = true
|
|
130
|
+
emit('update:modelValue', undefined)
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const parsed = JSON.parse(localJson.value)
|
|
136
|
+
error.value = null
|
|
137
|
+
isValid.value = true
|
|
138
|
+
emit('update:modelValue', parsed)
|
|
139
|
+
} catch (e) {
|
|
140
|
+
error.value = e.message
|
|
141
|
+
isValid.value = false
|
|
142
|
+
// Do not emit invalid values
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Watch external changes only if they differ significantly from local
|
|
147
|
+
/*
|
|
148
|
+
Note: two-way binding with text representation is tricky.
|
|
149
|
+
If we watch props.modelValue, we might re-format user's in-progress typing if we aren't careful.
|
|
150
|
+
Usually better to only update localJson if the prop changes "from outside".
|
|
151
|
+
For this simple tool, initial value is likely enough, or we can watch with a deep compare check.
|
|
152
|
+
For now, let's stick to initial + internal validation.
|
|
153
|
+
*/
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
localJson,
|
|
157
|
+
error,
|
|
158
|
+
isValid,
|
|
159
|
+
validate
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const Tools = {
|
|
83
165
|
template: `
|
|
84
166
|
<div class="p-4 md:p-6 max-w-7xl mx-auto w-full relative">
|
|
85
167
|
<div v-if="Object.keys($ctx.tools.toolPageHeaders).length">
|
|
@@ -153,6 +235,10 @@ const Tools = {
|
|
|
153
235
|
</select>
|
|
154
236
|
</div>
|
|
155
237
|
|
|
238
|
+
<div v-else-if="prop.type === 'object' || prop.type === 'array'">
|
|
239
|
+
<JsonInput v-model="execForm[name]" />
|
|
240
|
+
</div>
|
|
241
|
+
|
|
156
242
|
<div v-else>
|
|
157
243
|
<input :type="prop.type === 'integer' || prop.type === 'number' ? 'number' : 'text'"
|
|
158
244
|
v-model="execForm[name]"
|
|
@@ -615,6 +701,8 @@ export default {
|
|
|
615
701
|
ctx.components({
|
|
616
702
|
Tools,
|
|
617
703
|
ToolSelector,
|
|
704
|
+
ToolResult,
|
|
705
|
+
JsonInput,
|
|
618
706
|
})
|
|
619
707
|
|
|
620
708
|
ctx.setGlobals({
|