llms-py 3.0.18__py3-none-any.whl → 3.0.20__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.
@@ -223,7 +223,10 @@ def install(ctx):
223
223
  await ctx.chat_completion(chat_req, context=context_req)
224
224
  except Exception as ex:
225
225
  ctx.err("run_chat", ex)
226
- # not necessary to update thread in db with error as it's done in chat_error filter
226
+ # shouldn't be necessary to update thread in db with error as it's done in chat_error filter
227
+ thread = thread_dto(g_db.get_thread(id, user=ctx.get_username(request)))
228
+ if thread and not thread.get("error"):
229
+ await chat_error(ex, context)
227
230
 
228
231
  asyncio.create_task(run_chat(chat, context))
229
232
 
@@ -406,7 +406,7 @@ def directory_tree(
406
406
  ) -> str:
407
407
  """
408
408
  Get a recursive tree view of files and directories as a JSON structure. Each entry includes 'name', 'type' (file/directory), and 'children' for directories.
409
- Files have no children array, while directories always have a children array (which may be empty).
409
+ Files have no children array, while directories always have a children array (which may be empty). Respects any .gitignore rules from the root directory together with any exclude_patterns.
410
410
  The output is formatted with 2-space indentation for readability. Only works within allowed directories.
411
411
  """
412
412
  import json
@@ -416,6 +416,24 @@ def directory_tree(
416
416
  if exclude_patterns is None:
417
417
  exclude_patterns = []
418
418
 
419
+ def _parse_gitignore(directory: str) -> List[str]:
420
+ gitignore_path = os.path.join(directory, ".gitignore")
421
+ patterns = []
422
+ if os.path.exists(gitignore_path) and os.path.isfile(gitignore_path):
423
+ try:
424
+ with open(gitignore_path, encoding="utf-8") as f:
425
+ for line in f:
426
+ line = line.strip()
427
+ if line and not line.startswith("#"):
428
+ patterns.append(line)
429
+ except Exception as e:
430
+ logger.warning(f"Error reading .gitignore in {directory}: {e}")
431
+ return patterns
432
+
433
+ # Parse .gitignore only in the root directory (Simple Global Mappings)
434
+ gitignore_patterns = _parse_gitignore(valid_path)
435
+ all_exclude_patterns = exclude_patterns + gitignore_patterns
436
+
419
437
  def _build_tree(current_path: str) -> List[Dict[str, Any]]:
420
438
  entries = []
421
439
  try:
@@ -423,14 +441,27 @@ def directory_tree(
423
441
  items = sorted(it, key=lambda x: x.name)
424
442
 
425
443
  for entry in items:
426
- # Check exclusion
427
- rel_path = entry.path[root_path_len:]
428
- # Check against patterns
444
+ # 1. Check exclusion patterns
445
+ rel_path_from_root = entry.path[root_path_len:]
446
+
429
447
  should_exclude = False
430
- for pattern in exclude_patterns:
431
- if fnmatch.fnmatch(rel_path, pattern) or fnmatch.fnmatch(entry.name, pattern):
448
+ for pattern in all_exclude_patterns:
449
+ # Match against relative path or name
450
+ # Support ending with / for directory matching
451
+ is_dir_pattern = pattern.endswith("/")
452
+ norm_pattern = pattern.rstrip("/")
453
+
454
+ if fnmatch.fnmatch(rel_path_from_root, pattern) or fnmatch.fnmatch(entry.name, pattern):
432
455
  should_exclude = True
433
456
  break
457
+
458
+ # Handle patterns like "node_modules/" matching "node_modules" directory
459
+ if fnmatch.fnmatch(entry.name, norm_pattern):
460
+ if is_dir_pattern and not entry.is_dir():
461
+ continue
462
+ should_exclude = True
463
+ break
464
+
434
465
  if should_exclude:
435
466
  continue
436
467
 
@@ -18,6 +18,11 @@ def install_openai(ctx):
18
18
  super().__init__(**kwargs)
19
19
  self.modalities["image"] = OpenAiGenerator(**kwargs)
20
20
 
21
+ async def process_chat(self, chat, provider_id=None):
22
+ ret = await super().process_chat(chat, provider_id)
23
+ chat.pop("modalities", None) # openai chat completion doesn't support modalities
24
+ return ret
25
+
21
26
  # https://platform.openai.com/docs/api-reference/images
22
27
  class OpenAiGenerator(GeneratorBase):
23
28
  sdk = "openai/image"
llms/main.py CHANGED
@@ -57,7 +57,7 @@ try:
57
57
  except ImportError:
58
58
  HAS_PIL = False
59
59
 
60
- VERSION = "3.0.18"
60
+ VERSION = "3.0.20"
61
61
  _ROOT = None
62
62
  DEBUG = os.getenv("DEBUG") == "1"
63
63
  MOCK = os.getenv("MOCK") == "1"
@@ -874,8 +874,18 @@ def save_image_to_cache(base64_data, filename, image_info, ignore_info=False):
874
874
  async def response_json(response):
875
875
  text = await response.text()
876
876
  if response.status >= 400:
877
- _dbg(f"HTTP {response.status} {response.reason}: {text}")
878
- raise HTTPError(response.status, reason=response.reason, body=text, headers=dict(response.headers))
877
+ message = "HTTP " + str(response.status) + " " + response.reason
878
+ _dbg(f"HTTP {response.status} {response.reason}\n{dict(response.headers)}\n{text}")
879
+ try:
880
+ body = json.loads(text)
881
+ if "message" in body:
882
+ message = body["message"]
883
+ elif "error" in body:
884
+ message = body["error"]
885
+ except Exception:
886
+ if text:
887
+ message += ": " + text[:100]
888
+ raise Exception(message)
879
889
  response.raise_for_status()
880
890
  body = json.loads(text)
881
891
  return body
@@ -1305,7 +1315,7 @@ class GroqProvider(OpenAiCompatible):
1305
1315
  super().__init__(**kwargs)
1306
1316
 
1307
1317
  async def process_chat(self, chat, provider_id=None):
1308
- ret = await process_chat(chat, provider_id)
1318
+ ret = await super().process_chat(chat, provider_id)
1309
1319
  chat.pop("modalities", None) # groq doesn't support modalities
1310
1320
  messages = chat.get("messages", []).copy()
1311
1321
  for message in messages:
@@ -1946,12 +1956,11 @@ async def g_chat_completion(chat, context=None):
1946
1956
  first_exception = e
1947
1957
  context["stackTrace"] = traceback.format_exc()
1948
1958
  _err(f"Provider {provider_name} failed", first_exception)
1949
- await g_app.on_chat_error(e, context)
1950
-
1951
1959
  continue
1952
1960
 
1953
1961
  # If we get here, all providers failed
1954
1962
  if first_exception:
1963
+ await g_app.on_chat_error(first_exception, context or {"chat": chat})
1955
1964
  raise first_exception
1956
1965
 
1957
1966
  e = Exception("All providers failed")
@@ -3871,9 +3880,9 @@ def cli_exec(cli_args, extra_args):
3871
3880
  asyncio.run(update_extensions(cli_args.update))
3872
3881
  return ExitCode.SUCCESS
3873
3882
 
3883
+ g_app.add_allowed_directory(tempfile.gettempdir()) # add temp directory
3874
3884
  g_app.add_allowed_directory(home_llms_path(".agent")) # info for agents, e.g: skills
3875
3885
  g_app.add_allowed_directory(os.getcwd()) # add current directory
3876
- g_app.add_allowed_directory(tempfile.gettempdir()) # add temp directory
3877
3886
 
3878
3887
  g_app.extensions = install_extensions()
3879
3888
 
llms/ui/ai.mjs CHANGED
@@ -6,7 +6,7 @@ const headers = { 'Accept': 'application/json' }
6
6
  const prefsKey = 'llms.prefs'
7
7
 
8
8
  export const o = {
9
- version: '3.0.18',
9
+ version: '3.0.20',
10
10
  base,
11
11
  prefsKey,
12
12
  welcome: 'Welcome to llms.py',