AstrBot 4.11.4__py3-none-any.whl → 4.12.1__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.
Files changed (41) hide show
  1. astrbot/cli/__init__.py +1 -1
  2. astrbot/core/agent/runners/tool_loop_agent_runner.py +10 -8
  3. astrbot/core/config/default.py +66 -2
  4. astrbot/core/db/__init__.py +84 -2
  5. astrbot/core/db/po.py +65 -0
  6. astrbot/core/db/sqlite.py +225 -4
  7. astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +103 -49
  8. astrbot/core/pipeline/process_stage/utils.py +40 -0
  9. astrbot/core/platform/astr_message_event.py +23 -4
  10. astrbot/core/platform/sources/discord/discord_platform_adapter.py +2 -0
  11. astrbot/core/platform/sources/telegram/tg_adapter.py +2 -0
  12. astrbot/core/platform/sources/webchat/webchat_adapter.py +3 -2
  13. astrbot/core/platform/sources/webchat/webchat_event.py +17 -4
  14. astrbot/core/provider/sources/anthropic_source.py +44 -0
  15. astrbot/core/sandbox/booters/base.py +31 -0
  16. astrbot/core/sandbox/booters/boxlite.py +186 -0
  17. astrbot/core/sandbox/booters/shipyard.py +67 -0
  18. astrbot/core/sandbox/olayer/__init__.py +5 -0
  19. astrbot/core/sandbox/olayer/filesystem.py +33 -0
  20. astrbot/core/sandbox/olayer/python.py +19 -0
  21. astrbot/core/sandbox/olayer/shell.py +21 -0
  22. astrbot/core/sandbox/sandbox_client.py +52 -0
  23. astrbot/core/sandbox/tools/__init__.py +10 -0
  24. astrbot/core/sandbox/tools/fs.py +188 -0
  25. astrbot/core/sandbox/tools/python.py +74 -0
  26. astrbot/core/sandbox/tools/shell.py +55 -0
  27. astrbot/core/star/context.py +162 -44
  28. astrbot/dashboard/routes/__init__.py +2 -0
  29. astrbot/dashboard/routes/chat.py +40 -12
  30. astrbot/dashboard/routes/chatui_project.py +245 -0
  31. astrbot/dashboard/routes/session_management.py +545 -0
  32. astrbot/dashboard/server.py +1 -0
  33. {astrbot-4.11.4.dist-info → astrbot-4.12.1.dist-info}/METADATA +2 -1
  34. {astrbot-4.11.4.dist-info → astrbot-4.12.1.dist-info}/RECORD +37 -28
  35. astrbot/builtin_stars/python_interpreter/main.py +0 -536
  36. astrbot/builtin_stars/python_interpreter/metadata.yaml +0 -4
  37. astrbot/builtin_stars/python_interpreter/requirements.txt +0 -1
  38. astrbot/builtin_stars/python_interpreter/shared/api.py +0 -22
  39. {astrbot-4.11.4.dist-info → astrbot-4.12.1.dist-info}/WHEEL +0 -0
  40. {astrbot-4.11.4.dist-info → astrbot-4.12.1.dist-info}/entry_points.txt +0 -0
  41. {astrbot-4.11.4.dist-info → astrbot-4.12.1.dist-info}/licenses/LICENSE +0 -0
@@ -35,6 +35,14 @@ class SessionManagementRoute(Route):
35
35
  "/session/delete-rule": ("POST", self.delete_session_rule),
36
36
  "/session/batch-delete-rule": ("POST", self.batch_delete_session_rule),
37
37
  "/session/active-umos": ("GET", self.list_umos),
38
+ "/session/list-all-with-status": ("GET", self.list_all_umos_with_status),
39
+ "/session/batch-update-service": ("POST", self.batch_update_service),
40
+ "/session/batch-update-provider": ("POST", self.batch_update_provider),
41
+ # 分组管理 API
42
+ "/session/groups": ("GET", self.list_groups),
43
+ "/session/group/create": ("POST", self.create_group),
44
+ "/session/group/update": ("POST", self.update_group),
45
+ "/session/group/delete": ("POST", self.delete_group),
38
46
  }
39
47
  self.conv_mgr = core_lifecycle.conversation_manager
40
48
  self.core_lifecycle = core_lifecycle
@@ -391,3 +399,540 @@ class SessionManagementRoute(Route):
391
399
  except Exception as e:
392
400
  logger.error(f"获取 UMO 列表失败: {e!s}")
393
401
  return Response().error(f"获取 UMO 列表失败: {e!s}").__dict__
402
+
403
+ async def list_all_umos_with_status(self):
404
+ """获取所有有对话记录的 UMO 及其服务状态(支持分页、搜索、筛选)
405
+
406
+ Query 参数:
407
+ page: 页码,默认为 1
408
+ page_size: 每页数量,默认为 20
409
+ search: 搜索关键词
410
+ message_type: 筛选消息类型 (group/private/all)
411
+ platform: 筛选平台
412
+ """
413
+ try:
414
+ page = request.args.get("page", 1, type=int)
415
+ page_size = request.args.get("page_size", 20, type=int)
416
+ search = request.args.get("search", "", type=str).strip()
417
+ message_type = request.args.get("message_type", "all", type=str)
418
+ platform = request.args.get("platform", "", type=str)
419
+
420
+ if page < 1:
421
+ page = 1
422
+ if page_size < 1:
423
+ page_size = 20
424
+ if page_size > 100:
425
+ page_size = 100
426
+
427
+ # 从 Conversation 表获取所有 distinct user_id (即 umo)
428
+ async with self.db_helper.get_db() as session:
429
+ session: AsyncSession
430
+ result = await session.execute(
431
+ select(ConversationV2.user_id)
432
+ .distinct()
433
+ .order_by(ConversationV2.user_id)
434
+ )
435
+ all_umos = [row[0] for row in result.fetchall()]
436
+
437
+ # 获取所有 umo 的规则配置
438
+ umo_rules, _ = await self._get_umo_rules(page=1, page_size=99999, search="")
439
+
440
+ # 构建带状态的 umo 列表
441
+ umos_with_status = []
442
+ for umo in all_umos:
443
+ parts = umo.split(":")
444
+ umo_platform = parts[0] if len(parts) >= 1 else "unknown"
445
+ umo_message_type = parts[1] if len(parts) >= 2 else "unknown"
446
+ umo_session_id = parts[2] if len(parts) >= 3 else umo
447
+
448
+ # 筛选消息类型
449
+ if message_type != "all":
450
+ if message_type == "group" and umo_message_type not in [
451
+ "group",
452
+ "GroupMessage",
453
+ ]:
454
+ continue
455
+ if message_type == "private" and umo_message_type not in [
456
+ "private",
457
+ "FriendMessage",
458
+ "friend",
459
+ ]:
460
+ continue
461
+
462
+ # 筛选平台
463
+ if platform and umo_platform != platform:
464
+ continue
465
+
466
+ # 获取服务配置
467
+ rules = umo_rules.get(umo, {})
468
+ svc_config = rules.get("session_service_config", {})
469
+
470
+ custom_name = svc_config.get("custom_name", "") if svc_config else ""
471
+ session_enabled = (
472
+ svc_config.get("session_enabled", True) if svc_config else True
473
+ )
474
+ llm_enabled = (
475
+ svc_config.get("llm_enabled", True) if svc_config else True
476
+ )
477
+ tts_enabled = (
478
+ svc_config.get("tts_enabled", True) if svc_config else True
479
+ )
480
+
481
+ # 搜索过滤
482
+ if search:
483
+ search_lower = search.lower()
484
+ if (
485
+ search_lower not in umo.lower()
486
+ and search_lower not in custom_name.lower()
487
+ ):
488
+ continue
489
+
490
+ # 获取 provider 配置
491
+ chat_provider_key = (
492
+ f"provider_perf_{ProviderType.CHAT_COMPLETION.value}"
493
+ )
494
+ tts_provider_key = f"provider_perf_{ProviderType.TEXT_TO_SPEECH.value}"
495
+ stt_provider_key = f"provider_perf_{ProviderType.SPEECH_TO_TEXT.value}"
496
+
497
+ umos_with_status.append(
498
+ {
499
+ "umo": umo,
500
+ "platform": umo_platform,
501
+ "message_type": umo_message_type,
502
+ "session_id": umo_session_id,
503
+ "custom_name": custom_name,
504
+ "session_enabled": session_enabled,
505
+ "llm_enabled": llm_enabled,
506
+ "tts_enabled": tts_enabled,
507
+ "has_rules": umo in umo_rules,
508
+ "chat_provider": rules.get(chat_provider_key),
509
+ "tts_provider": rules.get(tts_provider_key),
510
+ "stt_provider": rules.get(stt_provider_key),
511
+ }
512
+ )
513
+
514
+ # 分页
515
+ total = len(umos_with_status)
516
+ start_idx = (page - 1) * page_size
517
+ end_idx = start_idx + page_size
518
+ paginated = umos_with_status[start_idx:end_idx]
519
+
520
+ # 获取可用的平台列表
521
+ platforms = list({u["platform"] for u in umos_with_status})
522
+
523
+ # 获取可用的 providers
524
+ provider_manager = self.core_lifecycle.provider_manager
525
+ available_chat_providers = [
526
+ {"id": p.meta().id, "name": p.meta().id, "model": p.meta().model}
527
+ for p in provider_manager.provider_insts
528
+ ]
529
+ available_tts_providers = [
530
+ {"id": p.meta().id, "name": p.meta().id, "model": p.meta().model}
531
+ for p in provider_manager.tts_provider_insts
532
+ ]
533
+ available_stt_providers = [
534
+ {"id": p.meta().id, "name": p.meta().id, "model": p.meta().model}
535
+ for p in provider_manager.stt_provider_insts
536
+ ]
537
+
538
+ return (
539
+ Response()
540
+ .ok(
541
+ {
542
+ "sessions": paginated,
543
+ "total": total,
544
+ "page": page,
545
+ "page_size": page_size,
546
+ "platforms": platforms,
547
+ "available_chat_providers": available_chat_providers,
548
+ "available_tts_providers": available_tts_providers,
549
+ "available_stt_providers": available_stt_providers,
550
+ }
551
+ )
552
+ .__dict__
553
+ )
554
+ except Exception as e:
555
+ logger.error(f"获取会话状态列表失败: {e!s}")
556
+ return Response().error(f"获取会话状态列表失败: {e!s}").__dict__
557
+
558
+ async def batch_update_service(self):
559
+ """批量更新多个 UMO 的服务状态 (LLM/TTS/Session)
560
+
561
+ 请求体:
562
+ {
563
+ "umos": ["平台:消息类型:会话ID", ...], // 可选,如果不传则根据 scope 筛选
564
+ "scope": "all" | "group" | "private" | "custom_group", // 可选,批量范围
565
+ "group_id": "分组ID", // 当 scope 为 custom_group 时必填
566
+ "llm_enabled": true/false/null, // 可选,null表示不修改
567
+ "tts_enabled": true/false/null, // 可选
568
+ "session_enabled": true/false/null // 可选
569
+ }
570
+ """
571
+ try:
572
+ data = await request.get_json()
573
+ umos = data.get("umos", [])
574
+ scope = data.get("scope", "")
575
+ group_id = data.get("group_id", "")
576
+ llm_enabled = data.get("llm_enabled")
577
+ tts_enabled = data.get("tts_enabled")
578
+ session_enabled = data.get("session_enabled")
579
+
580
+ # 如果没有任何修改
581
+ if llm_enabled is None and tts_enabled is None and session_enabled is None:
582
+ return Response().error("至少需要指定一个要修改的状态").__dict__
583
+
584
+ # 如果指定了 scope,获取符合条件的所有 umo
585
+ if scope and not umos:
586
+ # 如果是自定义分组
587
+ if scope == "custom_group":
588
+ if not group_id:
589
+ return Response().error("请指定分组 ID").__dict__
590
+ groups = self._get_groups()
591
+ if group_id not in groups:
592
+ return Response().error(f"分组 '{group_id}' 不存在").__dict__
593
+ umos = groups[group_id].get("umos", [])
594
+ else:
595
+ async with self.db_helper.get_db() as session:
596
+ session: AsyncSession
597
+ result = await session.execute(
598
+ select(ConversationV2.user_id).distinct()
599
+ )
600
+ all_umos = [row[0] for row in result.fetchall()]
601
+
602
+ if scope == "group":
603
+ umos = [
604
+ u
605
+ for u in all_umos
606
+ if ":group:" in u.lower() or ":groupmessage:" in u.lower()
607
+ ]
608
+ elif scope == "private":
609
+ umos = [
610
+ u
611
+ for u in all_umos
612
+ if ":private:" in u.lower() or ":friend" in u.lower()
613
+ ]
614
+ elif scope == "all":
615
+ umos = all_umos
616
+
617
+ if not umos:
618
+ return Response().error("没有找到符合条件的会话").__dict__
619
+
620
+ # 批量更新
621
+ success_count = 0
622
+ failed_umos = []
623
+
624
+ for umo in umos:
625
+ try:
626
+ # 获取现有配置
627
+ session_config = (
628
+ sp.get("session_service_config", {}, scope="umo", scope_id=umo)
629
+ or {}
630
+ )
631
+
632
+ # 更新状态
633
+ if llm_enabled is not None:
634
+ session_config["llm_enabled"] = llm_enabled
635
+ if tts_enabled is not None:
636
+ session_config["tts_enabled"] = tts_enabled
637
+ if session_enabled is not None:
638
+ session_config["session_enabled"] = session_enabled
639
+
640
+ # 保存
641
+ sp.put(
642
+ "session_service_config",
643
+ session_config,
644
+ scope="umo",
645
+ scope_id=umo,
646
+ )
647
+ success_count += 1
648
+ except Exception as e:
649
+ logger.error(f"更新 {umo} 服务状态失败: {e!s}")
650
+ failed_umos.append(umo)
651
+
652
+ status_changes = []
653
+ if llm_enabled is not None:
654
+ status_changes.append(f"LLM={'启用' if llm_enabled else '禁用'}")
655
+ if tts_enabled is not None:
656
+ status_changes.append(f"TTS={'启用' if tts_enabled else '禁用'}")
657
+ if session_enabled is not None:
658
+ status_changes.append(f"会话={'启用' if session_enabled else '禁用'}")
659
+
660
+ return (
661
+ Response()
662
+ .ok(
663
+ {
664
+ "message": f"已更新 {success_count} 个会话 ({', '.join(status_changes)})",
665
+ "success_count": success_count,
666
+ "failed_count": len(failed_umos),
667
+ "failed_umos": failed_umos,
668
+ }
669
+ )
670
+ .__dict__
671
+ )
672
+ except Exception as e:
673
+ logger.error(f"批量更新服务状态失败: {e!s}")
674
+ return Response().error(f"批量更新服务状态失败: {e!s}").__dict__
675
+
676
+ async def batch_update_provider(self):
677
+ """批量更新多个 UMO 的 Provider 配置
678
+
679
+ 请求体:
680
+ {
681
+ "umos": ["平台:消息类型:会话ID", ...], // 可选
682
+ "scope": "all" | "group" | "private", // 可选
683
+ "provider_type": "chat_completion" | "text_to_speech" | "speech_to_text",
684
+ "provider_id": "provider_id"
685
+ }
686
+ """
687
+ try:
688
+ data = await request.get_json()
689
+ umos = data.get("umos", [])
690
+ scope = data.get("scope", "")
691
+ provider_type = data.get("provider_type")
692
+ provider_id = data.get("provider_id")
693
+
694
+ if not provider_type or not provider_id:
695
+ return (
696
+ Response()
697
+ .error("缺少必要参数: provider_type, provider_id")
698
+ .__dict__
699
+ )
700
+
701
+ # 转换 provider_type
702
+ provider_type_map = {
703
+ "chat_completion": ProviderType.CHAT_COMPLETION,
704
+ "text_to_speech": ProviderType.TEXT_TO_SPEECH,
705
+ "speech_to_text": ProviderType.SPEECH_TO_TEXT,
706
+ }
707
+ if provider_type not in provider_type_map:
708
+ return (
709
+ Response()
710
+ .error(f"不支持的 provider_type: {provider_type}")
711
+ .__dict__
712
+ )
713
+
714
+ provider_type_enum = provider_type_map[provider_type]
715
+
716
+ # 如果指定了 scope,获取符合条件的所有 umo
717
+ group_id = data.get("group_id", "")
718
+ if scope and not umos:
719
+ # 如果是自定义分组
720
+ if scope == "custom_group":
721
+ if not group_id:
722
+ return Response().error("请指定分组 ID").__dict__
723
+ groups = self._get_groups()
724
+ if group_id not in groups:
725
+ return Response().error(f"分组 '{group_id}' 不存在").__dict__
726
+ umos = groups[group_id].get("umos", [])
727
+ else:
728
+ async with self.db_helper.get_db() as session:
729
+ session: AsyncSession
730
+ result = await session.execute(
731
+ select(ConversationV2.user_id).distinct()
732
+ )
733
+ all_umos = [row[0] for row in result.fetchall()]
734
+
735
+ if scope == "group":
736
+ umos = [
737
+ u
738
+ for u in all_umos
739
+ if ":group:" in u.lower() or ":groupmessage:" in u.lower()
740
+ ]
741
+ elif scope == "private":
742
+ umos = [
743
+ u
744
+ for u in all_umos
745
+ if ":private:" in u.lower() or ":friend" in u.lower()
746
+ ]
747
+ elif scope == "all":
748
+ umos = all_umos
749
+
750
+ if not umos:
751
+ return Response().error("没有找到符合条件的会话").__dict__
752
+
753
+ # 批量更新
754
+ success_count = 0
755
+ failed_umos = []
756
+ provider_manager = self.core_lifecycle.provider_manager
757
+
758
+ for umo in umos:
759
+ try:
760
+ await provider_manager.set_provider(
761
+ provider_id=provider_id,
762
+ provider_type=provider_type_enum,
763
+ umo=umo,
764
+ )
765
+ success_count += 1
766
+ except Exception as e:
767
+ logger.error(f"更新 {umo} Provider 失败: {e!s}")
768
+ failed_umos.append(umo)
769
+
770
+ return (
771
+ Response()
772
+ .ok(
773
+ {
774
+ "message": f"已更新 {success_count} 个会话的 {provider_type} 为 {provider_id}",
775
+ "success_count": success_count,
776
+ "failed_count": len(failed_umos),
777
+ "failed_umos": failed_umos,
778
+ }
779
+ )
780
+ .__dict__
781
+ )
782
+ except Exception as e:
783
+ logger.error(f"批量更新 Provider 失败: {e!s}")
784
+ return Response().error(f"批量更新 Provider 失败: {e!s}").__dict__
785
+
786
+ # ==================== 分组管理 API ====================
787
+
788
+ def _get_groups(self) -> dict:
789
+ """获取所有分组"""
790
+ return sp.get("session_groups", {})
791
+
792
+ def _save_groups(self, groups: dict) -> None:
793
+ """保存分组"""
794
+ sp.put("session_groups", groups)
795
+
796
+ async def list_groups(self):
797
+ """获取所有分组列表"""
798
+ try:
799
+ groups = self._get_groups()
800
+ # 转换为列表格式,方便前端使用
801
+ groups_list = []
802
+ for group_id, group_data in groups.items():
803
+ groups_list.append(
804
+ {
805
+ "id": group_id,
806
+ "name": group_data.get("name", ""),
807
+ "umos": group_data.get("umos", []),
808
+ "umo_count": len(group_data.get("umos", [])),
809
+ }
810
+ )
811
+ return Response().ok({"groups": groups_list}).__dict__
812
+ except Exception as e:
813
+ logger.error(f"获取分组列表失败: {e!s}")
814
+ return Response().error(f"获取分组列表失败: {e!s}").__dict__
815
+
816
+ async def create_group(self):
817
+ """创建新分组"""
818
+ try:
819
+ data = await request.json
820
+ name = data.get("name", "").strip()
821
+ umos = data.get("umos", [])
822
+
823
+ if not name:
824
+ return Response().error("分组名称不能为空").__dict__
825
+
826
+ groups = self._get_groups()
827
+
828
+ # 生成唯一 ID
829
+ import uuid
830
+
831
+ group_id = str(uuid.uuid4())[:8]
832
+
833
+ groups[group_id] = {
834
+ "name": name,
835
+ "umos": umos,
836
+ }
837
+
838
+ self._save_groups(groups)
839
+
840
+ return (
841
+ Response()
842
+ .ok(
843
+ {
844
+ "message": f"分组 '{name}' 创建成功",
845
+ "group": {
846
+ "id": group_id,
847
+ "name": name,
848
+ "umos": umos,
849
+ "umo_count": len(umos),
850
+ },
851
+ }
852
+ )
853
+ .__dict__
854
+ )
855
+ except Exception as e:
856
+ logger.error(f"创建分组失败: {e!s}")
857
+ return Response().error(f"创建分组失败: {e!s}").__dict__
858
+
859
+ async def update_group(self):
860
+ """更新分组(改名、增删成员)"""
861
+ try:
862
+ data = await request.json
863
+ group_id = data.get("id")
864
+ name = data.get("name")
865
+ umos = data.get("umos")
866
+ add_umos = data.get("add_umos", [])
867
+ remove_umos = data.get("remove_umos", [])
868
+
869
+ if not group_id:
870
+ return Response().error("分组 ID 不能为空").__dict__
871
+
872
+ groups = self._get_groups()
873
+
874
+ if group_id not in groups:
875
+ return Response().error(f"分组 '{group_id}' 不存在").__dict__
876
+
877
+ group = groups[group_id]
878
+
879
+ # 更新名称
880
+ if name is not None:
881
+ group["name"] = name.strip()
882
+
883
+ # 直接设置 umos 列表
884
+ if umos is not None:
885
+ group["umos"] = umos
886
+ else:
887
+ # 增量更新
888
+ current_umos = set(group.get("umos", []))
889
+ if add_umos:
890
+ current_umos.update(add_umos)
891
+ if remove_umos:
892
+ current_umos.difference_update(remove_umos)
893
+ group["umos"] = list(current_umos)
894
+
895
+ self._save_groups(groups)
896
+
897
+ return (
898
+ Response()
899
+ .ok(
900
+ {
901
+ "message": f"分组 '{group['name']}' 更新成功",
902
+ "group": {
903
+ "id": group_id,
904
+ "name": group["name"],
905
+ "umos": group["umos"],
906
+ "umo_count": len(group["umos"]),
907
+ },
908
+ }
909
+ )
910
+ .__dict__
911
+ )
912
+ except Exception as e:
913
+ logger.error(f"更新分组失败: {e!s}")
914
+ return Response().error(f"更新分组失败: {e!s}").__dict__
915
+
916
+ async def delete_group(self):
917
+ """删除分组"""
918
+ try:
919
+ data = await request.json
920
+ group_id = data.get("id")
921
+
922
+ if not group_id:
923
+ return Response().error("分组 ID 不能为空").__dict__
924
+
925
+ groups = self._get_groups()
926
+
927
+ if group_id not in groups:
928
+ return Response().error(f"分组 '{group_id}' 不存在").__dict__
929
+
930
+ group_name = groups[group_id].get("name", group_id)
931
+ del groups[group_id]
932
+
933
+ self._save_groups(groups)
934
+
935
+ return Response().ok({"message": f"分组 '{group_name}' 已删除"}).__dict__
936
+ except Exception as e:
937
+ logger.error(f"删除分组失败: {e!s}")
938
+ return Response().error(f"删除分组失败: {e!s}").__dict__
@@ -74,6 +74,7 @@ class AstrBotDashboard:
74
74
  self.sfr = StaticFileRoute(self.context)
75
75
  self.ar = AuthRoute(self.context)
76
76
  self.chat_route = ChatRoute(self.context, db, core_lifecycle)
77
+ self.chatui_project_route = ChatUIProjectRoute(self.context, db)
77
78
  self.tools_root = ToolsRoute(self.context, core_lifecycle)
78
79
  self.conversation_route = ConversationRoute(self.context, db, core_lifecycle)
79
80
  self.file_route = FileRoute(self.context)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: AstrBot
3
- Version: 4.11.4
3
+ Version: 4.12.1
4
4
  Summary: Easy-to-use multi-platform LLM chatbot and development framework
5
5
  License-File: LICENSE
6
6
  Keywords: Astrbot,Astrbot Module,Astrbot Plugin
@@ -47,6 +47,7 @@ Requires-Dist: qq-botpy>=1.2.1
47
47
  Requires-Dist: quart>=0.20.0
48
48
  Requires-Dist: rank-bm25>=0.2.2
49
49
  Requires-Dist: readability-lxml>=0.8.4.1
50
+ Requires-Dist: shipyard-python-sdk>=0.2.4
50
51
  Requires-Dist: silk-python>=0.2.6
51
52
  Requires-Dist: slack-sdk>=3.35.0
52
53
  Requires-Dist: sqlalchemy[asyncio]>=2.0.41