kimi-agent-module-api 1.1.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.1.0/src/kimi_agent_module_api.egg-info → kimi_agent_module_api-1.2.0}/PKG-INFO +6 -1
- {kimi_agent_module_api-1.1.0 → kimi_agent_module_api-1.2.0}/README.md +5 -0
- {kimi_agent_module_api-1.1.0 → kimi_agent_module_api-1.2.0}/pyproject.toml +1 -1
- {kimi_agent_module_api-1.1.0 → kimi_agent_module_api-1.2.0}/src/kimi_agent_module_api/__init__.py +20 -0
- {kimi_agent_module_api-1.1.0 → kimi_agent_module_api-1.2.0}/src/kimi_agent_module_api/contracts.py +187 -1
- {kimi_agent_module_api-1.1.0 → kimi_agent_module_api-1.2.0}/src/kimi_agent_module_api/testing.py +45 -3
- {kimi_agent_module_api-1.1.0 → kimi_agent_module_api-1.2.0/src/kimi_agent_module_api.egg-info}/PKG-INFO +6 -1
- {kimi_agent_module_api-1.1.0 → kimi_agent_module_api-1.2.0}/LICENSE +0 -0
- {kimi_agent_module_api-1.1.0 → kimi_agent_module_api-1.2.0}/setup.cfg +0 -0
- {kimi_agent_module_api-1.1.0 → kimi_agent_module_api-1.2.0}/src/kimi_agent_module_api/events.py +0 -0
- {kimi_agent_module_api-1.1.0 → kimi_agent_module_api-1.2.0}/src/kimi_agent_module_api/images.py +0 -0
- {kimi_agent_module_api-1.1.0 → kimi_agent_module_api-1.2.0}/src/kimi_agent_module_api/py.typed +0 -0
- {kimi_agent_module_api-1.1.0 → kimi_agent_module_api-1.2.0}/src/kimi_agent_module_api/settings.py +0 -0
- {kimi_agent_module_api-1.1.0 → kimi_agent_module_api-1.2.0}/src/kimi_agent_module_api/tools.py +0 -0
- {kimi_agent_module_api-1.1.0 → kimi_agent_module_api-1.2.0}/src/kimi_agent_module_api/trust.py +0 -0
- {kimi_agent_module_api-1.1.0 → kimi_agent_module_api-1.2.0}/src/kimi_agent_module_api.egg-info/SOURCES.txt +0 -0
- {kimi_agent_module_api-1.1.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.1.0 → kimi_agent_module_api-1.2.0}/src/kimi_agent_module_api.egg-info/requires.txt +0 -0
- {kimi_agent_module_api-1.1.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
|
|
@@ -54,3 +54,8 @@ Guild-scoped live command replacement was added in 1.1. Modules using
|
|
|
54
54
|
`InteractionRouter.replace_guild_commands()` should depend on
|
|
55
55
|
`kimi-agent-module-api>=1.1,<2` and require the host capability
|
|
56
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`.
|
|
@@ -37,3 +37,8 @@ Guild-scoped live command replacement was added in 1.1. Modules using
|
|
|
37
37
|
`InteractionRouter.replace_guild_commands()` should depend on
|
|
38
38
|
`kimi-agent-module-api>=1.1,<2` and require the host capability
|
|
39
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.1.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.1.0 → kimi_agent_module_api-1.2.0}/src/kimi_agent_module_api/contracts.py
RENAMED
|
@@ -819,6 +819,82 @@ class OutgoingEmbed:
|
|
|
819
819
|
timestamp: bool = False
|
|
820
820
|
|
|
821
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
|
+
|
|
822
898
|
class DiscordActions(Protocol):
|
|
823
899
|
"""Declared Discord operations on stable IDs.
|
|
824
900
|
|
|
@@ -957,6 +1033,9 @@ class ModuleInteraction(Protocol):
|
|
|
957
1033
|
@property
|
|
958
1034
|
def values(self) -> tuple[str, ...]: ...
|
|
959
1035
|
|
|
1036
|
+
@property
|
|
1037
|
+
def text_values(self) -> Mapping[str, str]: ...
|
|
1038
|
+
|
|
960
1039
|
@property
|
|
961
1040
|
def message(self) -> MessageRef | None:
|
|
962
1041
|
"""The message a button or select lives on; ``None`` for slash commands."""
|
|
@@ -967,17 +1046,21 @@ class ModuleInteraction(Protocol):
|
|
|
967
1046
|
content: str | None = None,
|
|
968
1047
|
*,
|
|
969
1048
|
embed: OutgoingEmbed | None = None,
|
|
1049
|
+
layout: OutgoingLayout | None = None,
|
|
970
1050
|
ephemeral: bool = False,
|
|
971
1051
|
components: Sequence[Any] = (),
|
|
972
1052
|
) -> None: ...
|
|
973
1053
|
|
|
974
1054
|
async def defer(self, *, ephemeral: bool = False) -> None: ...
|
|
975
1055
|
|
|
1056
|
+
async def show_modal(self, modal: ModalSpec) -> None: ...
|
|
1057
|
+
|
|
976
1058
|
async def edit_original(
|
|
977
1059
|
self,
|
|
978
1060
|
content: str | None = None,
|
|
979
1061
|
*,
|
|
980
1062
|
embed: OutgoingEmbed | None = None,
|
|
1063
|
+
layout: OutgoingLayout | None = None,
|
|
981
1064
|
components: Sequence[Any] = (),
|
|
982
1065
|
) -> None: ...
|
|
983
1066
|
|
|
@@ -1013,11 +1096,114 @@ class SelectSpec:
|
|
|
1013
1096
|
max_values: int = 1
|
|
1014
1097
|
|
|
1015
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
|
+
|
|
1016
1202
|
type CommandHandler = Callable[[ModuleInteraction], Awaitable[None]]
|
|
1017
1203
|
type AutocompleteHandler = Callable[
|
|
1018
1204
|
[ModuleInteraction, str, str], Awaitable[Sequence[tuple[str, str | int]]]
|
|
1019
1205
|
]
|
|
1020
|
-
type ComponentKind = Literal["button", "select"]
|
|
1206
|
+
type ComponentKind = Literal["button", "select", "modal"]
|
|
1021
1207
|
|
|
1022
1208
|
|
|
1023
1209
|
@dataclass(frozen=True, slots=True)
|
{kimi_agent_module_api-1.1.0 → kimi_agent_module_api-1.2.0}/src/kimi_agent_module_api/testing.py
RENAMED
|
@@ -45,11 +45,13 @@ from kimi_agent_module_api.contracts import (
|
|
|
45
45
|
MessagePage,
|
|
46
46
|
MessageRef,
|
|
47
47
|
MessageSnapshot,
|
|
48
|
+
ModalSpec,
|
|
48
49
|
TABLE_NAME_RE,
|
|
49
50
|
MigrationContext,
|
|
50
51
|
ModuleContractError,
|
|
51
52
|
ModuleHealth,
|
|
52
53
|
OutgoingEmbed,
|
|
54
|
+
OutgoingLayout,
|
|
53
55
|
ProposalActor,
|
|
54
56
|
ProposalError,
|
|
55
57
|
ProposalRef,
|
|
@@ -60,6 +62,9 @@ from kimi_agent_module_api.contracts import (
|
|
|
60
62
|
TrustTierName,
|
|
61
63
|
UndeclaredDiscordAction,
|
|
62
64
|
build_custom_id,
|
|
65
|
+
validate_modal_spec,
|
|
66
|
+
validate_layout_components,
|
|
67
|
+
validate_outgoing_layout,
|
|
63
68
|
validate_publish_topic,
|
|
64
69
|
)
|
|
65
70
|
from kimi_agent_module_api.tools import ModuleToolHandler
|
|
@@ -568,6 +573,7 @@ class FakeResponse:
|
|
|
568
573
|
ephemeral: bool
|
|
569
574
|
components: tuple[Any, ...]
|
|
570
575
|
kind: str
|
|
576
|
+
layout: OutgoingLayout | None = None
|
|
571
577
|
|
|
572
578
|
|
|
573
579
|
class FakeInteraction:
|
|
@@ -580,8 +586,10 @@ class FakeInteraction:
|
|
|
580
586
|
options: Mapping[str, Any] | None = None,
|
|
581
587
|
custom_id: str | None = None,
|
|
582
588
|
values: Sequence[str] = (),
|
|
589
|
+
text_values: Mapping[str, str] | None = None,
|
|
583
590
|
guild_name: str | None = "Test Guild",
|
|
584
591
|
message: MessageRef | None = None,
|
|
592
|
+
message_uses_layout: bool = False,
|
|
585
593
|
) -> None:
|
|
586
594
|
self._message = message
|
|
587
595
|
self._guild_name = guild_name
|
|
@@ -591,8 +599,11 @@ class FakeInteraction:
|
|
|
591
599
|
self._options = dict(options or {})
|
|
592
600
|
self._custom_id = custom_id
|
|
593
601
|
self._values = tuple(values)
|
|
602
|
+
self._text_values = dict(text_values or {})
|
|
594
603
|
self.responses: list[FakeResponse] = []
|
|
604
|
+
self.shown_modals: list[ModalSpec] = []
|
|
595
605
|
self.deferred: bool | None = None
|
|
606
|
+
self._original_uses_layout = message_uses_layout
|
|
596
607
|
|
|
597
608
|
@property
|
|
598
609
|
def guild_id(self) -> int:
|
|
@@ -622,6 +633,10 @@ class FakeInteraction:
|
|
|
622
633
|
def values(self) -> tuple[str, ...]:
|
|
623
634
|
return self._values
|
|
624
635
|
|
|
636
|
+
@property
|
|
637
|
+
def text_values(self) -> Mapping[str, str]:
|
|
638
|
+
return self._text_values
|
|
639
|
+
|
|
625
640
|
@property
|
|
626
641
|
def message(self) -> MessageRef | None:
|
|
627
642
|
return self._message
|
|
@@ -631,22 +646,49 @@ class FakeInteraction:
|
|
|
631
646
|
content: str | None = None,
|
|
632
647
|
*,
|
|
633
648
|
embed: OutgoingEmbed | None = None,
|
|
649
|
+
layout: OutgoingLayout | None = None,
|
|
634
650
|
ephemeral: bool = False,
|
|
635
651
|
components: Sequence[Any] = (),
|
|
636
652
|
) -> None:
|
|
637
|
-
|
|
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
|
|
638
662
|
|
|
639
663
|
async def defer(self, *, ephemeral: bool = False) -> None:
|
|
640
664
|
self.deferred = ephemeral
|
|
641
665
|
|
|
666
|
+
async def show_modal(self, modal: ModalSpec) -> None:
|
|
667
|
+
validate_modal_spec(modal)
|
|
668
|
+
self.shown_modals.append(modal)
|
|
669
|
+
|
|
642
670
|
async def edit_original(
|
|
643
671
|
self,
|
|
644
672
|
content: str | None = None,
|
|
645
673
|
*,
|
|
646
674
|
embed: OutgoingEmbed | None = None,
|
|
675
|
+
layout: OutgoingLayout | None = None,
|
|
647
676
|
components: Sequence[Any] = (),
|
|
648
677
|
) -> None:
|
|
649
|
-
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
|
|
650
692
|
|
|
651
693
|
async def follow_up(
|
|
652
694
|
self, content: str, *, embed: OutgoingEmbed | None = None, ephemeral: bool = False
|
|
@@ -742,7 +784,7 @@ class FakeInteractions:
|
|
|
742
784
|
expires_after_seconds: float | None = None,
|
|
743
785
|
min_tier: TrustTierName = "member",
|
|
744
786
|
) -> _Closable:
|
|
745
|
-
if kind not in ("button", "select"):
|
|
787
|
+
if kind not in ("button", "select", "modal"):
|
|
746
788
|
raise ModuleContractError(f"unsupported component kind {kind!r}")
|
|
747
789
|
build_custom_id(self.module_name, key)
|
|
748
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
|
|
@@ -54,3 +54,8 @@ Guild-scoped live command replacement was added in 1.1. Modules using
|
|
|
54
54
|
`InteractionRouter.replace_guild_commands()` should depend on
|
|
55
55
|
`kimi-agent-module-api>=1.1,<2` and require the host capability
|
|
56
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.1.0 → kimi_agent_module_api-1.2.0}/src/kimi_agent_module_api/events.py
RENAMED
|
File without changes
|
{kimi_agent_module_api-1.1.0 → kimi_agent_module_api-1.2.0}/src/kimi_agent_module_api/images.py
RENAMED
|
File without changes
|
{kimi_agent_module_api-1.1.0 → kimi_agent_module_api-1.2.0}/src/kimi_agent_module_api/py.typed
RENAMED
|
File without changes
|
{kimi_agent_module_api-1.1.0 → kimi_agent_module_api-1.2.0}/src/kimi_agent_module_api/settings.py
RENAMED
|
File without changes
|
{kimi_agent_module_api-1.1.0 → kimi_agent_module_api-1.2.0}/src/kimi_agent_module_api/tools.py
RENAMED
|
File without changes
|
{kimi_agent_module_api-1.1.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
|