kimi-agent-module-api 1.0.0__tar.gz → 1.2.0__tar.gz
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.
- {kimi_agent_module_api-1.0.0/src/kimi_agent_module_api.egg-info → kimi_agent_module_api-1.2.0}/PKG-INFO +12 -2
- {kimi_agent_module_api-1.0.0 → kimi_agent_module_api-1.2.0}/README.md +11 -1
- {kimi_agent_module_api-1.0.0 → kimi_agent_module_api-1.2.0}/pyproject.toml +1 -1
- {kimi_agent_module_api-1.0.0 → kimi_agent_module_api-1.2.0}/src/kimi_agent_module_api/__init__.py +20 -0
- {kimi_agent_module_api-1.0.0 → kimi_agent_module_api-1.2.0}/src/kimi_agent_module_api/contracts.py +208 -1
- {kimi_agent_module_api-1.0.0 → kimi_agent_module_api-1.2.0}/src/kimi_agent_module_api/testing.py +92 -3
- {kimi_agent_module_api-1.0.0 → kimi_agent_module_api-1.2.0/src/kimi_agent_module_api.egg-info}/PKG-INFO +12 -2
- {kimi_agent_module_api-1.0.0 → kimi_agent_module_api-1.2.0}/LICENSE +0 -0
- {kimi_agent_module_api-1.0.0 → kimi_agent_module_api-1.2.0}/setup.cfg +0 -0
- {kimi_agent_module_api-1.0.0 → kimi_agent_module_api-1.2.0}/src/kimi_agent_module_api/events.py +0 -0
- {kimi_agent_module_api-1.0.0 → kimi_agent_module_api-1.2.0}/src/kimi_agent_module_api/images.py +0 -0
- {kimi_agent_module_api-1.0.0 → kimi_agent_module_api-1.2.0}/src/kimi_agent_module_api/py.typed +0 -0
- {kimi_agent_module_api-1.0.0 → kimi_agent_module_api-1.2.0}/src/kimi_agent_module_api/settings.py +0 -0
- {kimi_agent_module_api-1.0.0 → kimi_agent_module_api-1.2.0}/src/kimi_agent_module_api/tools.py +0 -0
- {kimi_agent_module_api-1.0.0 → kimi_agent_module_api-1.2.0}/src/kimi_agent_module_api/trust.py +0 -0
- {kimi_agent_module_api-1.0.0 → kimi_agent_module_api-1.2.0}/src/kimi_agent_module_api.egg-info/SOURCES.txt +0 -0
- {kimi_agent_module_api-1.0.0 → kimi_agent_module_api-1.2.0}/src/kimi_agent_module_api.egg-info/dependency_links.txt +0 -0
- {kimi_agent_module_api-1.0.0 → kimi_agent_module_api-1.2.0}/src/kimi_agent_module_api.egg-info/requires.txt +0 -0
- {kimi_agent_module_api-1.0.0 → kimi_agent_module_api-1.2.0}/src/kimi_agent_module_api.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kimi-agent-module-api
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Stable contracts for community-built assistant modules
|
|
5
5
|
Author: Webhead
|
|
6
6
|
License-Expression: MIT
|
|
@@ -48,4 +48,14 @@ my_module = "my_module_package:SPEC"
|
|
|
48
48
|
The [module guide](https://github.com/webhead2oo9/kimi-agent/blob/main/docs/modules.md)
|
|
49
49
|
documents installation, declarations, lifecycle, and every runtime port. The
|
|
50
50
|
[reference module](https://github.com/webhead2oo9/kimi-agent/tree/main/bot/modules/example)
|
|
51
|
-
is a complete, commented example that exercises
|
|
51
|
+
is a complete, commented example that exercises most ports; start there.
|
|
52
|
+
|
|
53
|
+
Guild-scoped live command replacement was added in 1.1. Modules using
|
|
54
|
+
`InteractionRouter.replace_guild_commands()` should depend on
|
|
55
|
+
`kimi-agent-module-api>=1.1,<2` and require the host capability
|
|
56
|
+
`discord.guild_commands.v1`.
|
|
57
|
+
|
|
58
|
+
Version 1.2 adds typed modal forms and a narrow Components V2 layout model. Once a response uses
|
|
59
|
+
that layout model, Discord requires every later edit of the same message to remain a layout.
|
|
60
|
+
Modules using them should depend on `kimi-agent-module-api>=1.2,<2` and require
|
|
61
|
+
`discord.modals.v1` and/or `discord.components_v2.v1`.
|
|
@@ -31,4 +31,14 @@ my_module = "my_module_package:SPEC"
|
|
|
31
31
|
The [module guide](https://github.com/webhead2oo9/kimi-agent/blob/main/docs/modules.md)
|
|
32
32
|
documents installation, declarations, lifecycle, and every runtime port. The
|
|
33
33
|
[reference module](https://github.com/webhead2oo9/kimi-agent/tree/main/bot/modules/example)
|
|
34
|
-
is a complete, commented example that exercises
|
|
34
|
+
is a complete, commented example that exercises most ports; start there.
|
|
35
|
+
|
|
36
|
+
Guild-scoped live command replacement was added in 1.1. Modules using
|
|
37
|
+
`InteractionRouter.replace_guild_commands()` should depend on
|
|
38
|
+
`kimi-agent-module-api>=1.1,<2` and require the host capability
|
|
39
|
+
`discord.guild_commands.v1`.
|
|
40
|
+
|
|
41
|
+
Version 1.2 adds typed modal forms and a narrow Components V2 layout model. Once a response uses
|
|
42
|
+
that layout model, Discord requires every later edit of the same message to remain a layout.
|
|
43
|
+
Modules using them should depend on `kimi-agent-module-api>=1.2,<2` and require
|
|
44
|
+
`discord.modals.v1` and/or `discord.components_v2.v1`.
|
{kimi_agent_module_api-1.0.0 → kimi_agent_module_api-1.2.0}/src/kimi_agent_module_api/__init__.py
RENAMED
|
@@ -20,9 +20,17 @@ from kimi_agent_module_api.contracts import (
|
|
|
20
20
|
HealthReporter,
|
|
21
21
|
InviteSnapshot,
|
|
22
22
|
InteractionRouter,
|
|
23
|
+
LayoutGallery,
|
|
24
|
+
LayoutItem,
|
|
25
|
+
LayoutSection,
|
|
26
|
+
LayoutSeparator,
|
|
27
|
+
LayoutSeparatorSpacing,
|
|
28
|
+
LayoutText,
|
|
29
|
+
ModalSpec,
|
|
23
30
|
ModuleHttp,
|
|
24
31
|
ModulePermissions,
|
|
25
32
|
ModuleStorage,
|
|
33
|
+
OutgoingLayout,
|
|
26
34
|
ScopedModuleMigration,
|
|
27
35
|
ProposalActor,
|
|
28
36
|
ProposalError,
|
|
@@ -34,6 +42,8 @@ from kimi_agent_module_api.contracts import (
|
|
|
34
42
|
ServiceRegistry,
|
|
35
43
|
ServiceRequirement,
|
|
36
44
|
TrustLookup,
|
|
45
|
+
TextInputSpec,
|
|
46
|
+
TextInputStyle,
|
|
37
47
|
)
|
|
38
48
|
from kimi_agent_module_api.settings import ModuleSetting, ModuleSettingsDefinition
|
|
39
49
|
from kimi_agent_module_api.tools import (
|
|
@@ -151,6 +161,13 @@ __all__ = [
|
|
|
151
161
|
"ConfigSnapshot",
|
|
152
162
|
"GuildSettingsSchema",
|
|
153
163
|
"InviteSnapshot",
|
|
164
|
+
"LayoutGallery",
|
|
165
|
+
"LayoutItem",
|
|
166
|
+
"LayoutSection",
|
|
167
|
+
"LayoutSeparator",
|
|
168
|
+
"LayoutSeparatorSpacing",
|
|
169
|
+
"LayoutText",
|
|
170
|
+
"ModalSpec",
|
|
154
171
|
"ModuleCapabilities",
|
|
155
172
|
"ModuleLoadContext",
|
|
156
173
|
"ModulePermissions",
|
|
@@ -161,6 +178,7 @@ __all__ = [
|
|
|
161
178
|
"ModuleToolContext",
|
|
162
179
|
"ModuleToolHandler",
|
|
163
180
|
"ModuleToolRegistry",
|
|
181
|
+
"OutgoingLayout",
|
|
164
182
|
"ProposalActor",
|
|
165
183
|
"ProposalError",
|
|
166
184
|
"ProposalRef",
|
|
@@ -170,6 +188,8 @@ __all__ = [
|
|
|
170
188
|
"ScopedModuleMigration",
|
|
171
189
|
"ServiceDeclaration",
|
|
172
190
|
"ServiceRequirement",
|
|
191
|
+
"TextInputSpec",
|
|
192
|
+
"TextInputStyle",
|
|
173
193
|
"TrustTier",
|
|
174
194
|
"render_guild_settings",
|
|
175
195
|
]
|
{kimi_agent_module_api-1.0.0 → kimi_agent_module_api-1.2.0}/src/kimi_agent_module_api/contracts.py
RENAMED
|
@@ -52,6 +52,10 @@ class ServiceUnavailable(RuntimeError):
|
|
|
52
52
|
"""Raised through a service proxy after its provider module closed."""
|
|
53
53
|
|
|
54
54
|
|
|
55
|
+
class CommandSyncError(RuntimeError):
|
|
56
|
+
"""Discord did not accept a live guild-command synchronization."""
|
|
57
|
+
|
|
58
|
+
|
|
55
59
|
# --------------------------------------------------------------------------
|
|
56
60
|
# Declarations carried on ModuleSpec
|
|
57
61
|
# --------------------------------------------------------------------------
|
|
@@ -815,6 +819,82 @@ class OutgoingEmbed:
|
|
|
815
819
|
timestamp: bool = False
|
|
816
820
|
|
|
817
821
|
|
|
822
|
+
@dataclass(frozen=True, slots=True)
|
|
823
|
+
class LayoutText:
|
|
824
|
+
content: str
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
type LayoutSeparatorSpacing = Literal["small", "large"]
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
@dataclass(frozen=True, slots=True)
|
|
831
|
+
class LayoutSeparator:
|
|
832
|
+
visible: bool = True
|
|
833
|
+
spacing: LayoutSeparatorSpacing = "small"
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
@dataclass(frozen=True, slots=True)
|
|
837
|
+
class LayoutGallery:
|
|
838
|
+
urls: tuple[str, ...]
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
@dataclass(frozen=True, slots=True)
|
|
842
|
+
class LayoutSection:
|
|
843
|
+
texts: tuple[str, ...]
|
|
844
|
+
thumbnail_url: str
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
type LayoutItem = LayoutText | LayoutSeparator | LayoutGallery | LayoutSection
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
@dataclass(frozen=True, slots=True)
|
|
851
|
+
class OutgoingLayout:
|
|
852
|
+
"""One Components V2 container, optionally followed by interactive controls."""
|
|
853
|
+
|
|
854
|
+
items: tuple[LayoutItem, ...]
|
|
855
|
+
accent_color: int | None = None
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
def validate_outgoing_layout(layout: OutgoingLayout) -> None:
|
|
859
|
+
"""Validate the Discord hard limits represented by ``OutgoingLayout``."""
|
|
860
|
+
if not 1 <= len(layout.items) <= 40:
|
|
861
|
+
raise ModuleContractError("a layout must contain between one and 40 items")
|
|
862
|
+
if layout.accent_color is not None and (
|
|
863
|
+
isinstance(layout.accent_color, bool) or not 0 <= layout.accent_color <= 0xFFFFFF
|
|
864
|
+
):
|
|
865
|
+
raise ModuleContractError("layout accent_color must be between 0 and 0xFFFFFF")
|
|
866
|
+
|
|
867
|
+
def validate_text(text: Any, label: str) -> None:
|
|
868
|
+
if not isinstance(text, str) or not 1 <= len(text) <= 4_000:
|
|
869
|
+
raise ModuleContractError(f"{label} must contain between one and 4000 characters")
|
|
870
|
+
|
|
871
|
+
text_length = 0
|
|
872
|
+
for item in layout.items:
|
|
873
|
+
if isinstance(item, LayoutText):
|
|
874
|
+
validate_text(item.content, "layout text")
|
|
875
|
+
text_length += len(item.content)
|
|
876
|
+
elif isinstance(item, LayoutSeparator):
|
|
877
|
+
if item.spacing not in ("small", "large"):
|
|
878
|
+
raise ModuleContractError(f"invalid layout separator spacing {item.spacing!r}")
|
|
879
|
+
elif isinstance(item, LayoutGallery):
|
|
880
|
+
if not 1 <= len(item.urls) <= 10:
|
|
881
|
+
raise ModuleContractError("a layout gallery must contain between one and 10 URLs")
|
|
882
|
+
if any(not isinstance(url, str) or not url for url in item.urls):
|
|
883
|
+
raise ModuleContractError("layout gallery URLs must be non-empty strings")
|
|
884
|
+
elif isinstance(item, LayoutSection):
|
|
885
|
+
if not 1 <= len(item.texts) <= 3:
|
|
886
|
+
raise ModuleContractError("a layout section must contain between one and 3 texts")
|
|
887
|
+
for text in item.texts:
|
|
888
|
+
validate_text(text, "layout section text")
|
|
889
|
+
text_length += len(text)
|
|
890
|
+
if not isinstance(item.thumbnail_url, str) or not item.thumbnail_url:
|
|
891
|
+
raise ModuleContractError("a layout section thumbnail URL must be non-empty")
|
|
892
|
+
else:
|
|
893
|
+
raise ModuleContractError(f"unsupported layout item {item!r}")
|
|
894
|
+
if text_length > 4_000:
|
|
895
|
+
raise ModuleContractError("layout text cannot exceed 4000 characters in total")
|
|
896
|
+
|
|
897
|
+
|
|
818
898
|
class DiscordActions(Protocol):
|
|
819
899
|
"""Declared Discord operations on stable IDs.
|
|
820
900
|
|
|
@@ -953,6 +1033,9 @@ class ModuleInteraction(Protocol):
|
|
|
953
1033
|
@property
|
|
954
1034
|
def values(self) -> tuple[str, ...]: ...
|
|
955
1035
|
|
|
1036
|
+
@property
|
|
1037
|
+
def text_values(self) -> Mapping[str, str]: ...
|
|
1038
|
+
|
|
956
1039
|
@property
|
|
957
1040
|
def message(self) -> MessageRef | None:
|
|
958
1041
|
"""The message a button or select lives on; ``None`` for slash commands."""
|
|
@@ -963,17 +1046,21 @@ class ModuleInteraction(Protocol):
|
|
|
963
1046
|
content: str | None = None,
|
|
964
1047
|
*,
|
|
965
1048
|
embed: OutgoingEmbed | None = None,
|
|
1049
|
+
layout: OutgoingLayout | None = None,
|
|
966
1050
|
ephemeral: bool = False,
|
|
967
1051
|
components: Sequence[Any] = (),
|
|
968
1052
|
) -> None: ...
|
|
969
1053
|
|
|
970
1054
|
async def defer(self, *, ephemeral: bool = False) -> None: ...
|
|
971
1055
|
|
|
1056
|
+
async def show_modal(self, modal: ModalSpec) -> None: ...
|
|
1057
|
+
|
|
972
1058
|
async def edit_original(
|
|
973
1059
|
self,
|
|
974
1060
|
content: str | None = None,
|
|
975
1061
|
*,
|
|
976
1062
|
embed: OutgoingEmbed | None = None,
|
|
1063
|
+
layout: OutgoingLayout | None = None,
|
|
977
1064
|
components: Sequence[Any] = (),
|
|
978
1065
|
) -> None: ...
|
|
979
1066
|
|
|
@@ -1009,11 +1096,123 @@ class SelectSpec:
|
|
|
1009
1096
|
max_values: int = 1
|
|
1010
1097
|
|
|
1011
1098
|
|
|
1099
|
+
def validate_layout_components(
|
|
1100
|
+
components: Sequence[Any], *, layout: OutgoingLayout | None = None
|
|
1101
|
+
) -> None:
|
|
1102
|
+
"""Validate control rows and the full Components V2 descendant limit."""
|
|
1103
|
+
rows = 0
|
|
1104
|
+
buttons_in_row = 0
|
|
1105
|
+
for component in components:
|
|
1106
|
+
if isinstance(component, ButtonSpec):
|
|
1107
|
+
if buttons_in_row == 0:
|
|
1108
|
+
rows += 1
|
|
1109
|
+
buttons_in_row = (buttons_in_row + 1) % 5
|
|
1110
|
+
elif isinstance(component, SelectSpec):
|
|
1111
|
+
buttons_in_row = 0
|
|
1112
|
+
rows += 1
|
|
1113
|
+
else:
|
|
1114
|
+
raise ModuleContractError(f"unsupported component {component!r}")
|
|
1115
|
+
if rows > 5:
|
|
1116
|
+
raise ModuleContractError("layout controls cannot require more than five action rows")
|
|
1117
|
+
if layout is not None:
|
|
1118
|
+
descendants = 1 + rows + len(components)
|
|
1119
|
+
for item in layout.items:
|
|
1120
|
+
descendants += len(item.texts) + 2 if isinstance(item, LayoutSection) else 1
|
|
1121
|
+
if descendants > 40:
|
|
1122
|
+
raise ModuleContractError("a layout cannot exceed 40 components in total")
|
|
1123
|
+
|
|
1124
|
+
|
|
1125
|
+
type TextInputStyle = Literal["short", "paragraph"]
|
|
1126
|
+
|
|
1127
|
+
|
|
1128
|
+
@dataclass(frozen=True, slots=True)
|
|
1129
|
+
class TextInputSpec:
|
|
1130
|
+
key: str
|
|
1131
|
+
label: str
|
|
1132
|
+
style: TextInputStyle = "short"
|
|
1133
|
+
default: str | None = None
|
|
1134
|
+
placeholder: str | None = None
|
|
1135
|
+
required: bool = True
|
|
1136
|
+
min_length: int | None = None
|
|
1137
|
+
max_length: int | None = None
|
|
1138
|
+
|
|
1139
|
+
|
|
1140
|
+
@dataclass(frozen=True, slots=True)
|
|
1141
|
+
class ModalSpec:
|
|
1142
|
+
"""A modal whose submit handler is registered under ``key``."""
|
|
1143
|
+
|
|
1144
|
+
key: str
|
|
1145
|
+
title: str
|
|
1146
|
+
inputs: tuple[TextInputSpec, ...]
|
|
1147
|
+
parts: tuple[str, ...] = ()
|
|
1148
|
+
|
|
1149
|
+
|
|
1150
|
+
def validate_modal_spec(modal: ModalSpec) -> None:
|
|
1151
|
+
"""Validate Discord's hard modal and text-input limits."""
|
|
1152
|
+
if not isinstance(modal.key, str) or not _TOPIC_SEGMENT_RE.fullmatch(modal.key):
|
|
1153
|
+
raise ModuleContractError(f"invalid modal key {modal.key!r}")
|
|
1154
|
+
if any(not isinstance(part, str) or ":" in part for part in modal.parts):
|
|
1155
|
+
raise ModuleContractError("modal custom_id parts must be strings without ':'")
|
|
1156
|
+
if not isinstance(modal.title, str) or not 1 <= len(modal.title) <= 45:
|
|
1157
|
+
raise ModuleContractError("a modal title must contain between one and 45 characters")
|
|
1158
|
+
if not 1 <= len(modal.inputs) <= 5:
|
|
1159
|
+
raise ModuleContractError("a modal must contain between one and five text inputs")
|
|
1160
|
+
|
|
1161
|
+
keys: set[str] = set()
|
|
1162
|
+
for input_spec in modal.inputs:
|
|
1163
|
+
if not isinstance(input_spec.key, str) or not _TOPIC_SEGMENT_RE.fullmatch(input_spec.key):
|
|
1164
|
+
raise ModuleContractError(f"invalid modal text input key {input_spec.key!r}")
|
|
1165
|
+
if input_spec.key in keys:
|
|
1166
|
+
raise ModuleContractError("modal text input keys must be unique")
|
|
1167
|
+
keys.add(input_spec.key)
|
|
1168
|
+
if not isinstance(input_spec.label, str) or not 1 <= len(input_spec.label) <= 45:
|
|
1169
|
+
raise ModuleContractError(
|
|
1170
|
+
"a modal text input label must contain between one and 45 characters"
|
|
1171
|
+
)
|
|
1172
|
+
if input_spec.style not in ("short", "paragraph"):
|
|
1173
|
+
raise ModuleContractError(f"invalid modal text input style {input_spec.style!r}")
|
|
1174
|
+
if input_spec.placeholder is not None and (
|
|
1175
|
+
not isinstance(input_spec.placeholder, str) or len(input_spec.placeholder) > 100
|
|
1176
|
+
):
|
|
1177
|
+
raise ModuleContractError("a modal text input placeholder cannot exceed 100 characters")
|
|
1178
|
+
if input_spec.default is not None and (
|
|
1179
|
+
not isinstance(input_spec.default, str) or len(input_spec.default) > 4_000
|
|
1180
|
+
):
|
|
1181
|
+
raise ModuleContractError("a modal text input default cannot exceed 4000 characters")
|
|
1182
|
+
if input_spec.min_length is not None and (
|
|
1183
|
+
isinstance(input_spec.min_length, bool)
|
|
1184
|
+
or not isinstance(input_spec.min_length, int)
|
|
1185
|
+
or not 0 <= input_spec.min_length <= 4_000
|
|
1186
|
+
):
|
|
1187
|
+
raise ModuleContractError("modal text input min_length must be between 0 and 4000")
|
|
1188
|
+
if input_spec.max_length is not None and (
|
|
1189
|
+
isinstance(input_spec.max_length, bool)
|
|
1190
|
+
or not isinstance(input_spec.max_length, int)
|
|
1191
|
+
or not 1 <= input_spec.max_length <= 4_000
|
|
1192
|
+
):
|
|
1193
|
+
raise ModuleContractError("modal text input max_length must be between 1 and 4000")
|
|
1194
|
+
if (
|
|
1195
|
+
input_spec.min_length is not None
|
|
1196
|
+
and input_spec.max_length is not None
|
|
1197
|
+
and input_spec.min_length > input_spec.max_length
|
|
1198
|
+
):
|
|
1199
|
+
raise ModuleContractError("modal text input min_length cannot exceed max_length")
|
|
1200
|
+
|
|
1201
|
+
|
|
1012
1202
|
type CommandHandler = Callable[[ModuleInteraction], Awaitable[None]]
|
|
1013
1203
|
type AutocompleteHandler = Callable[
|
|
1014
1204
|
[ModuleInteraction, str, str], Awaitable[Sequence[tuple[str, str | int]]]
|
|
1015
1205
|
]
|
|
1016
|
-
type ComponentKind = Literal["button", "select"]
|
|
1206
|
+
type ComponentKind = Literal["button", "select", "modal"]
|
|
1207
|
+
|
|
1208
|
+
|
|
1209
|
+
@dataclass(frozen=True, slots=True)
|
|
1210
|
+
class GuildCommand:
|
|
1211
|
+
"""One command in a module's desired command set for a guild."""
|
|
1212
|
+
|
|
1213
|
+
spec: CommandSpec
|
|
1214
|
+
handler: CommandHandler
|
|
1215
|
+
autocomplete: AutocompleteHandler | None = None
|
|
1017
1216
|
|
|
1018
1217
|
|
|
1019
1218
|
class Registration(Protocol):
|
|
@@ -1029,6 +1228,14 @@ class InteractionRouter(Protocol):
|
|
|
1029
1228
|
autocomplete: AutocompleteHandler | None = None,
|
|
1030
1229
|
) -> Registration: ...
|
|
1031
1230
|
|
|
1231
|
+
async def replace_guild_commands(
|
|
1232
|
+
self,
|
|
1233
|
+
guild_id: int,
|
|
1234
|
+
commands: Sequence[GuildCommand],
|
|
1235
|
+
) -> None:
|
|
1236
|
+
"""Replace this module's complete command set for one guild."""
|
|
1237
|
+
...
|
|
1238
|
+
|
|
1032
1239
|
def register_component(
|
|
1033
1240
|
self,
|
|
1034
1241
|
kind: ComponentKind,
|
{kimi_agent_module_api-1.0.0 → kimi_agent_module_api-1.2.0}/src/kimi_agent_module_api/testing.py
RENAMED
|
@@ -29,6 +29,7 @@ from kimi_agent_module_api.contracts import (
|
|
|
29
29
|
Backoff,
|
|
30
30
|
ChannelSnapshot,
|
|
31
31
|
CommandSpec,
|
|
32
|
+
GuildCommand,
|
|
32
33
|
ConfigSnapshot,
|
|
33
34
|
Event,
|
|
34
35
|
EventHandler,
|
|
@@ -44,11 +45,13 @@ from kimi_agent_module_api.contracts import (
|
|
|
44
45
|
MessagePage,
|
|
45
46
|
MessageRef,
|
|
46
47
|
MessageSnapshot,
|
|
48
|
+
ModalSpec,
|
|
47
49
|
TABLE_NAME_RE,
|
|
48
50
|
MigrationContext,
|
|
49
51
|
ModuleContractError,
|
|
50
52
|
ModuleHealth,
|
|
51
53
|
OutgoingEmbed,
|
|
54
|
+
OutgoingLayout,
|
|
52
55
|
ProposalActor,
|
|
53
56
|
ProposalError,
|
|
54
57
|
ProposalRef,
|
|
@@ -59,6 +62,9 @@ from kimi_agent_module_api.contracts import (
|
|
|
59
62
|
TrustTierName,
|
|
60
63
|
UndeclaredDiscordAction,
|
|
61
64
|
build_custom_id,
|
|
65
|
+
validate_modal_spec,
|
|
66
|
+
validate_layout_components,
|
|
67
|
+
validate_outgoing_layout,
|
|
62
68
|
validate_publish_topic,
|
|
63
69
|
)
|
|
64
70
|
from kimi_agent_module_api.tools import ModuleToolHandler
|
|
@@ -567,6 +573,7 @@ class FakeResponse:
|
|
|
567
573
|
ephemeral: bool
|
|
568
574
|
components: tuple[Any, ...]
|
|
569
575
|
kind: str
|
|
576
|
+
layout: OutgoingLayout | None = None
|
|
570
577
|
|
|
571
578
|
|
|
572
579
|
class FakeInteraction:
|
|
@@ -579,8 +586,10 @@ class FakeInteraction:
|
|
|
579
586
|
options: Mapping[str, Any] | None = None,
|
|
580
587
|
custom_id: str | None = None,
|
|
581
588
|
values: Sequence[str] = (),
|
|
589
|
+
text_values: Mapping[str, str] | None = None,
|
|
582
590
|
guild_name: str | None = "Test Guild",
|
|
583
591
|
message: MessageRef | None = None,
|
|
592
|
+
message_uses_layout: bool = False,
|
|
584
593
|
) -> None:
|
|
585
594
|
self._message = message
|
|
586
595
|
self._guild_name = guild_name
|
|
@@ -590,8 +599,11 @@ class FakeInteraction:
|
|
|
590
599
|
self._options = dict(options or {})
|
|
591
600
|
self._custom_id = custom_id
|
|
592
601
|
self._values = tuple(values)
|
|
602
|
+
self._text_values = dict(text_values or {})
|
|
593
603
|
self.responses: list[FakeResponse] = []
|
|
604
|
+
self.shown_modals: list[ModalSpec] = []
|
|
594
605
|
self.deferred: bool | None = None
|
|
606
|
+
self._original_uses_layout = message_uses_layout
|
|
595
607
|
|
|
596
608
|
@property
|
|
597
609
|
def guild_id(self) -> int:
|
|
@@ -621,6 +633,10 @@ class FakeInteraction:
|
|
|
621
633
|
def values(self) -> tuple[str, ...]:
|
|
622
634
|
return self._values
|
|
623
635
|
|
|
636
|
+
@property
|
|
637
|
+
def text_values(self) -> Mapping[str, str]:
|
|
638
|
+
return self._text_values
|
|
639
|
+
|
|
624
640
|
@property
|
|
625
641
|
def message(self) -> MessageRef | None:
|
|
626
642
|
return self._message
|
|
@@ -630,22 +646,49 @@ class FakeInteraction:
|
|
|
630
646
|
content: str | None = None,
|
|
631
647
|
*,
|
|
632
648
|
embed: OutgoingEmbed | None = None,
|
|
649
|
+
layout: OutgoingLayout | None = None,
|
|
633
650
|
ephemeral: bool = False,
|
|
634
651
|
components: Sequence[Any] = (),
|
|
635
652
|
) -> None:
|
|
636
|
-
|
|
653
|
+
if layout is not None and (content is not None or embed is not None):
|
|
654
|
+
raise ModuleContractError("layout cannot be combined with content or embed")
|
|
655
|
+
if layout is not None:
|
|
656
|
+
validate_outgoing_layout(layout)
|
|
657
|
+
validate_layout_components(components, layout=layout)
|
|
658
|
+
self.responses.append(
|
|
659
|
+
FakeResponse(content, embed, ephemeral, tuple(components), "respond", layout=layout)
|
|
660
|
+
)
|
|
661
|
+
self._original_uses_layout = layout is not None
|
|
637
662
|
|
|
638
663
|
async def defer(self, *, ephemeral: bool = False) -> None:
|
|
639
664
|
self.deferred = ephemeral
|
|
640
665
|
|
|
666
|
+
async def show_modal(self, modal: ModalSpec) -> None:
|
|
667
|
+
validate_modal_spec(modal)
|
|
668
|
+
self.shown_modals.append(modal)
|
|
669
|
+
|
|
641
670
|
async def edit_original(
|
|
642
671
|
self,
|
|
643
672
|
content: str | None = None,
|
|
644
673
|
*,
|
|
645
674
|
embed: OutgoingEmbed | None = None,
|
|
675
|
+
layout: OutgoingLayout | None = None,
|
|
646
676
|
components: Sequence[Any] = (),
|
|
647
677
|
) -> None:
|
|
648
|
-
self.
|
|
678
|
+
if self._original_uses_layout and layout is None:
|
|
679
|
+
raise ModuleContractError(
|
|
680
|
+
"a Components V2 message must continue to use layout when edited"
|
|
681
|
+
)
|
|
682
|
+
if layout is not None and (content is not None or embed is not None):
|
|
683
|
+
raise ModuleContractError("layout cannot be combined with content or embed")
|
|
684
|
+
if layout is not None:
|
|
685
|
+
validate_outgoing_layout(layout)
|
|
686
|
+
validate_layout_components(components, layout=layout)
|
|
687
|
+
self.responses.append(
|
|
688
|
+
FakeResponse(content, embed, False, tuple(components), "edit", layout=layout)
|
|
689
|
+
)
|
|
690
|
+
if layout is not None:
|
|
691
|
+
self._original_uses_layout = True
|
|
649
692
|
|
|
650
693
|
async def follow_up(
|
|
651
694
|
self, content: str, *, embed: OutgoingEmbed | None = None, ephemeral: bool = False
|
|
@@ -663,6 +706,8 @@ class FakeInteractions:
|
|
|
663
706
|
def __init__(self, module_name: str) -> None:
|
|
664
707
|
self.module_name = module_name
|
|
665
708
|
self.commands: dict[str, tuple[CommandSpec, Callable[..., Any]]] = {}
|
|
709
|
+
self.guild_commands: dict[int, dict[str, tuple[CommandSpec, Callable[..., Any]]]] = {}
|
|
710
|
+
self.guild_autocompletes: dict[int, dict[str, Callable[..., Any]]] = {}
|
|
666
711
|
self.components: dict[tuple[str, str], Callable[..., Any]] = {}
|
|
667
712
|
self.component_min_tiers: dict[tuple[str, str], TrustTierName] = {}
|
|
668
713
|
self.autocompletes: dict[str, Callable[..., Any]] = {}
|
|
@@ -686,6 +731,50 @@ class FakeInteractions:
|
|
|
686
731
|
lambda: (self.commands.pop(qualified, None), self.autocompletes.pop(qualified, None))
|
|
687
732
|
)
|
|
688
733
|
|
|
734
|
+
async def replace_guild_commands(
|
|
735
|
+
self,
|
|
736
|
+
guild_id: int,
|
|
737
|
+
commands: Sequence[GuildCommand],
|
|
738
|
+
) -> None:
|
|
739
|
+
if guild_id <= 0:
|
|
740
|
+
raise ModuleContractError("guild_id must be a positive Discord snowflake")
|
|
741
|
+
desired: dict[str, tuple[CommandSpec, Callable[..., Any]]] = {}
|
|
742
|
+
autocompletes: dict[str, Callable[..., Any]] = {}
|
|
743
|
+
top_kinds: dict[str, str] = {}
|
|
744
|
+
for command in commands:
|
|
745
|
+
top_name = command.spec.group or command.spec.name
|
|
746
|
+
kind = "group" if command.spec.group else "command"
|
|
747
|
+
if top_name in self.commands:
|
|
748
|
+
raise ModuleContractError(
|
|
749
|
+
f"module {self.module_name!r} guild command {top_name!r} "
|
|
750
|
+
"would shadow a global command"
|
|
751
|
+
)
|
|
752
|
+
previous_kind = top_kinds.setdefault(top_name, kind)
|
|
753
|
+
if previous_kind != kind:
|
|
754
|
+
raise ModuleContractError(
|
|
755
|
+
f"module {self.module_name!r} guild command {top_name!r} "
|
|
756
|
+
"is both a command and a group"
|
|
757
|
+
)
|
|
758
|
+
qualified = (
|
|
759
|
+
f"{command.spec.group}.{command.spec.name}"
|
|
760
|
+
if command.spec.group
|
|
761
|
+
else command.spec.name
|
|
762
|
+
)
|
|
763
|
+
if qualified in desired:
|
|
764
|
+
raise ModuleContractError(
|
|
765
|
+
f"module {self.module_name!r} guild command {qualified!r} "
|
|
766
|
+
"is registered more than once"
|
|
767
|
+
)
|
|
768
|
+
desired[qualified] = (command.spec, command.handler)
|
|
769
|
+
if command.autocomplete is not None:
|
|
770
|
+
autocompletes[qualified] = command.autocomplete
|
|
771
|
+
if desired:
|
|
772
|
+
self.guild_commands[guild_id] = desired
|
|
773
|
+
self.guild_autocompletes[guild_id] = autocompletes
|
|
774
|
+
else:
|
|
775
|
+
self.guild_commands.pop(guild_id, None)
|
|
776
|
+
self.guild_autocompletes.pop(guild_id, None)
|
|
777
|
+
|
|
689
778
|
def register_component(
|
|
690
779
|
self,
|
|
691
780
|
kind: str,
|
|
@@ -695,7 +784,7 @@ class FakeInteractions:
|
|
|
695
784
|
expires_after_seconds: float | None = None,
|
|
696
785
|
min_tier: TrustTierName = "member",
|
|
697
786
|
) -> _Closable:
|
|
698
|
-
if kind not in ("button", "select"):
|
|
787
|
+
if kind not in ("button", "select", "modal"):
|
|
699
788
|
raise ModuleContractError(f"unsupported component kind {kind!r}")
|
|
700
789
|
build_custom_id(self.module_name, key)
|
|
701
790
|
identity = (kind, key)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kimi-agent-module-api
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Stable contracts for community-built assistant modules
|
|
5
5
|
Author: Webhead
|
|
6
6
|
License-Expression: MIT
|
|
@@ -48,4 +48,14 @@ my_module = "my_module_package:SPEC"
|
|
|
48
48
|
The [module guide](https://github.com/webhead2oo9/kimi-agent/blob/main/docs/modules.md)
|
|
49
49
|
documents installation, declarations, lifecycle, and every runtime port. The
|
|
50
50
|
[reference module](https://github.com/webhead2oo9/kimi-agent/tree/main/bot/modules/example)
|
|
51
|
-
is a complete, commented example that exercises
|
|
51
|
+
is a complete, commented example that exercises most ports; start there.
|
|
52
|
+
|
|
53
|
+
Guild-scoped live command replacement was added in 1.1. Modules using
|
|
54
|
+
`InteractionRouter.replace_guild_commands()` should depend on
|
|
55
|
+
`kimi-agent-module-api>=1.1,<2` and require the host capability
|
|
56
|
+
`discord.guild_commands.v1`.
|
|
57
|
+
|
|
58
|
+
Version 1.2 adds typed modal forms and a narrow Components V2 layout model. Once a response uses
|
|
59
|
+
that layout model, Discord requires every later edit of the same message to remain a layout.
|
|
60
|
+
Modules using them should depend on `kimi-agent-module-api>=1.2,<2` and require
|
|
61
|
+
`discord.modals.v1` and/or `discord.components_v2.v1`.
|
|
File without changes
|
|
File without changes
|
{kimi_agent_module_api-1.0.0 → kimi_agent_module_api-1.2.0}/src/kimi_agent_module_api/events.py
RENAMED
|
File without changes
|
{kimi_agent_module_api-1.0.0 → kimi_agent_module_api-1.2.0}/src/kimi_agent_module_api/images.py
RENAMED
|
File without changes
|
{kimi_agent_module_api-1.0.0 → kimi_agent_module_api-1.2.0}/src/kimi_agent_module_api/py.typed
RENAMED
|
File without changes
|
{kimi_agent_module_api-1.0.0 → kimi_agent_module_api-1.2.0}/src/kimi_agent_module_api/settings.py
RENAMED
|
File without changes
|
{kimi_agent_module_api-1.0.0 → kimi_agent_module_api-1.2.0}/src/kimi_agent_module_api/tools.py
RENAMED
|
File without changes
|
{kimi_agent_module_api-1.0.0 → kimi_agent_module_api-1.2.0}/src/kimi_agent_module_api/trust.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|