llms-py 3.0.15__py3-none-any.whl → 3.0.16__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.
@@ -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
- if ctx.MOCK and "modalities" in chat:
364
- print("Mocking Google Gemini Image")
365
- with open(f"{ctx.MOCK_DIR}/gemini-image.json") as f:
366
- obj = json.load(f)
367
- else:
368
- try:
369
- async with session.post(
370
- gemini_chat_url,
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
- obj = json.loads(text)
383
- except:
384
- ctx.log(text)
385
- raise e
386
-
387
- if "error" in obj:
388
- ctx.log(f"Error: {obj['error']}")
389
- raise Exception(obj["error"]["message"])
390
-
391
- if ctx.debug:
392
- ctx.dbg(json.dumps(gemini_response_summary(obj), indent=2))
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 None,
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 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
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 Tools = {
80
- components: {
81
- ToolResult
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({