settings: Add setting to control permission for topic summarization.

This commit is contained in:
Sahil Batra
2025-02-10 16:46:37 +05:30
committed by Tim Abbott
parent 9b38444e42
commit 4ca28bb850
25 changed files with 247 additions and 6 deletions

View File

@@ -24,6 +24,10 @@ format used by the Zulip server that they are interacting with.
* [`POST /register`](/api/register-queue): Added * [`POST /register`](/api/register-queue): Added
`server_can_summarize_topics` to the response. `server_can_summarize_topics` to the response.
* [`POST /register`](/api/register-queue), [`POST /events`](/api/get-events),
`PATCH /realm`: Added `can_summarize_topics_group` realm setting which is
a [group-setting value](/api/group-setting-values) describing the set of
users with permission to use AI summarization.
**Feature level 349** **Feature level 349**

View File

@@ -256,6 +256,7 @@ export function build_page(): void {
giphy_help_link, giphy_help_link,
...get_realm_level_notification_settings(), ...get_realm_level_notification_settings(),
group_setting_labels: settings_config.all_group_setting_labels.realm, group_setting_labels: settings_config.all_group_setting_labels.realm,
server_can_summarize_topics: realm.server_can_summarize_topics,
}; };
const rendered_admin_tab = render_admin_tab(options); const rendered_admin_tab = render_admin_tab(options);

View File

@@ -56,6 +56,7 @@ export const realm_group_setting_name_schema = z.enum([
"can_manage_all_groups", "can_manage_all_groups",
"can_move_messages_between_channels_group", "can_move_messages_between_channels_group",
"can_move_messages_between_topics_group", "can_move_messages_between_topics_group",
"can_summarize_topics_group",
"create_multiuse_invite_group", "create_multiuse_invite_group",
"direct_message_initiator_group", "direct_message_initiator_group",
"direct_message_permission_group", "direct_message_permission_group",

View File

@@ -56,7 +56,6 @@ type TopicPopoverContext = {
topic_unmuted: boolean; topic_unmuted: boolean;
is_spectator: boolean; is_spectator: boolean;
is_moderator: boolean; is_moderator: boolean;
is_development_environment: boolean;
is_topic_empty: boolean; is_topic_empty: boolean;
can_move_topic: boolean; can_move_topic: boolean;
can_rename_topic: boolean; can_rename_topic: boolean;
@@ -67,6 +66,7 @@ type TopicPopoverContext = {
url: string; url: string;
visibility_policy: number | false; visibility_policy: number | false;
all_visibility_policies: AllVisibilityPolicies; all_visibility_policies: AllVisibilityPolicies;
can_summarize_topics: boolean;
}; };
type VisibilityChangePopoverContext = { type VisibilityChangePopoverContext = {
@@ -267,8 +267,6 @@ export function get_topic_popover_content_context({
can_move_topic, can_move_topic,
can_rename_topic, can_rename_topic,
is_moderator: current_user.is_moderator, is_moderator: current_user.is_moderator,
// Temporary, as we're using this to control whether we show the summarize popover.
is_development_environment: page_params.development_environment,
is_realm_admin: current_user.is_admin, is_realm_admin: current_user.is_admin,
topic_is_resolved: resolved_topic.is_resolved(topic_name), topic_is_resolved: resolved_topic.is_resolved(topic_name),
has_starred_messages, has_starred_messages,
@@ -276,6 +274,8 @@ export function get_topic_popover_content_context({
url, url,
visibility_policy, visibility_policy,
all_visibility_policies, all_visibility_policies,
can_summarize_topics:
realm.server_can_summarize_topics && settings_data.user_can_summarize_topics(),
}; };
} }

View File

@@ -225,6 +225,7 @@ export function dispatch_normal_event(event) {
can_manage_all_groups: noop, can_manage_all_groups: noop,
can_move_messages_between_channels_group: noop, can_move_messages_between_channels_group: noop,
can_move_messages_between_topics_group: noop, can_move_messages_between_topics_group: noop,
can_summarize_topics_group: noop,
create_multiuse_invite_group: noop, create_multiuse_invite_group: noop,
default_code_block_language: noop, default_code_block_language: noop,
default_language: noop, default_language: noop,

View File

@@ -814,6 +814,7 @@ export function check_realm_settings_property_changed(elem: HTMLElement): boolea
case "realm_can_manage_all_groups": case "realm_can_manage_all_groups":
case "realm_can_move_messages_between_channels_group": case "realm_can_move_messages_between_channels_group":
case "realm_can_move_messages_between_topics_group": case "realm_can_move_messages_between_topics_group":
case "realm_can_summarize_topics_group":
case "realm_create_multiuse_invite_group": case "realm_create_multiuse_invite_group":
case "realm_direct_message_initiator_group": case "realm_direct_message_initiator_group":
case "realm_direct_message_permission_group": { case "realm_direct_message_permission_group": {
@@ -1064,6 +1065,7 @@ export function populate_data_for_realm_settings_request(
"can_invite_users_group", "can_invite_users_group",
"can_move_messages_between_channels_group", "can_move_messages_between_channels_group",
"can_move_messages_between_topics_group", "can_move_messages_between_topics_group",
"can_summarize_topics_group",
"create_multiuse_invite_group", "create_multiuse_invite_group",
"direct_message_initiator_group", "direct_message_initiator_group",
"direct_message_permission_group", "direct_message_permission_group",
@@ -1534,6 +1536,7 @@ export const group_setting_widget_map = new Map<string, GroupSettingPillContaine
["realm_can_manage_all_groups", null], ["realm_can_manage_all_groups", null],
["realm_can_move_messages_between_channels_group", null], ["realm_can_move_messages_between_channels_group", null],
["realm_can_move_messages_between_topics_group", null], ["realm_can_move_messages_between_topics_group", null],
["realm_can_summarize_topics_group", null],
["realm_create_multiuse_invite_group", null], ["realm_create_multiuse_invite_group", null],
["realm_direct_message_initiator_group", null], ["realm_direct_message_initiator_group", null],
["realm_direct_message_permission_group", null], ["realm_direct_message_permission_group", null],

View File

@@ -687,6 +687,7 @@ export const all_group_setting_labels = {
can_access_all_users_group: $t({ can_access_all_users_group: $t({
defaultMessage: "Who can view all other users in the organization", defaultMessage: "Who can view all other users in the organization",
}), }),
can_summarize_topics_group: $t({defaultMessage: "Who can use AI summaries"}),
can_create_write_only_bots_group: $t({ can_create_write_only_bots_group: $t({
defaultMessage: "Who can create bots that send messages into Zulip", defaultMessage: "Who can create bots that send messages into Zulip",
}), }),
@@ -766,6 +767,7 @@ export const realm_group_permission_settings: {
subsection_heading: $t({defaultMessage: "Other permissions"}), subsection_heading: $t({defaultMessage: "Other permissions"}),
subsection_key: "org-other-permissions", subsection_key: "org-other-permissions",
settings: [ settings: [
"can_summarize_topics_group",
"can_create_write_only_bots_group", "can_create_write_only_bots_group",
"can_create_bots_group", "can_create_bots_group",
"can_add_custom_emoji_group", "can_add_custom_emoji_group",

View File

@@ -90,6 +90,14 @@ export function user_can_create_multiuse_invite(): boolean {
); );
} }
export function user_can_summarize_topics(): boolean {
return user_has_permission_for_group_setting(
realm.realm_can_summarize_topics_group,
"can_summarize_topics_group",
"realm",
);
}
export function can_subscribe_others_to_all_accessible_streams(): boolean { export function can_subscribe_others_to_all_accessible_streams(): boolean {
return user_has_permission_for_group_setting( return user_has_permission_for_group_setting(
realm.realm_can_add_subscribers_group, realm.realm_can_add_subscribers_group,

View File

@@ -533,6 +533,7 @@ export function discard_realm_property_element_changes(elem: HTMLElement): void
case "realm_can_manage_all_groups": case "realm_can_manage_all_groups":
case "realm_can_move_messages_between_channels_group": case "realm_can_move_messages_between_channels_group":
case "realm_can_move_messages_between_topics_group": case "realm_can_move_messages_between_topics_group":
case "realm_can_summarize_topics_group":
case "realm_create_multiuse_invite_group": case "realm_create_multiuse_invite_group":
case "realm_direct_message_initiator_group": case "realm_direct_message_initiator_group":
case "realm_direct_message_permission_group": { case "realm_direct_message_permission_group": {

View File

@@ -307,6 +307,7 @@ export const realm_schema = z.object({
realm_can_manage_all_groups: group_setting_value_schema, realm_can_manage_all_groups: group_setting_value_schema,
realm_can_move_messages_between_channels_group: group_setting_value_schema, realm_can_move_messages_between_channels_group: group_setting_value_schema,
realm_can_move_messages_between_topics_group: group_setting_value_schema, realm_can_move_messages_between_topics_group: group_setting_value_schema,
realm_can_summarize_topics_group: group_setting_value_schema,
realm_create_multiuse_invite_group: group_setting_value_schema, realm_create_multiuse_invite_group: group_setting_value_schema,
realm_date_created: z.number(), realm_date_created: z.number(),
realm_default_code_block_language: z.string(), realm_default_code_block_language: z.string(),

View File

@@ -506,6 +506,10 @@ input[type="checkbox"] {
padding-top: 10px; padding-top: 10px;
} }
#org-other-permissions .tip {
margin-top: 0;
}
.language_selection_widget .language_selection_button { .language_selection_widget .language_selection_button {
text-decoration: none; text-decoration: none;
min-width: 0; min-width: 0;

View File

@@ -38,7 +38,7 @@
{{else}} {{else}}
{{!-- Group 2 --}} {{!-- Group 2 --}}
<li role="separator" class="popover-menu-separator"></li> <li role="separator" class="popover-menu-separator"></li>
{{#if (and is_development_environment (or is_moderator is_realm_admin))}} {{#if can_summarize_topics}}
<li role="none" class="link-item popover-menu-list-item"> <li role="none" class="link-item popover-menu-list-item">
<a role="menuitem" class="sidebar-popover-summarize-topic popover-menu-link" tabindex="0"> <a role="menuitem" class="sidebar-popover-summarize-topic popover-menu-link" tabindex="0">
<i class="popover-menu-icon fa fa-magic" aria-hidden="true"></i> <i class="popover-menu-icon fa fa-magic" aria-hidden="true"></i>

View File

@@ -341,6 +341,15 @@
{{> settings_save_discard_widget section_name="other-permissions" }} {{> settings_save_discard_widget section_name="other-permissions" }}
</div> </div>
<div class="m-10 inline-block organization-permissions-parent"> <div class="m-10 inline-block organization-permissions-parent">
{{#unless server_can_summarize_topics}}
<div class="tip">
{{t "AI summaries are not enabled on this server."}}
</div>
{{/unless}}
{{> group_setting_value_pill_input
setting_name="realm_can_summarize_topics_group"
label=group_setting_labels.can_summarize_topics_group}}
{{> group_setting_value_pill_input {{> group_setting_value_pill_input
setting_name="realm_can_create_write_only_bots_group" setting_name="realm_can_create_write_only_bots_group"
label=group_setting_labels.can_create_write_only_bots_group}} label=group_setting_labels.can_create_write_only_bots_group}}

View File

@@ -632,3 +632,10 @@ run_test("guests_can_access_all_other_users", () => {
realm.realm_can_access_all_users_group = everyone.id; realm.realm_can_access_all_users_group = everyone.id;
assert.ok(settings_data.guests_can_access_all_other_users()); assert.ok(settings_data.guests_can_access_all_other_users());
}); });
run_test("user_can_summarize_topics", () => {
test_realm_group_settings(
"realm_can_summarize_topics_group",
settings_data.user_can_summarize_topics,
);
});

View File

@@ -524,6 +524,7 @@ class GroupSettingUpdateData(GroupSettingUpdateDataCore):
can_manage_all_groups: int | AnonymousSettingGroupDict | None = None can_manage_all_groups: int | AnonymousSettingGroupDict | None = None
can_move_messages_between_channels_group: int | AnonymousSettingGroupDict | None = None can_move_messages_between_channels_group: int | AnonymousSettingGroupDict | None = None
can_move_messages_between_topics_group: int | AnonymousSettingGroupDict | None = None can_move_messages_between_topics_group: int | AnonymousSettingGroupDict | None = None
can_summarize_topics_group: int | AnonymousSettingGroupDict | None = None
direct_message_initiator_group: int | AnonymousSettingGroupDict | None = None direct_message_initiator_group: int | AnonymousSettingGroupDict | None = None
direct_message_permission_group: int | AnonymousSettingGroupDict | None = None direct_message_permission_group: int | AnonymousSettingGroupDict | None = None

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.0.10 on 2025-02-10 10:31
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("zerver", "0663_realm_enable_guest_user_dm_warning"),
]
operations = [
migrations.AddField(
model_name="realm",
name="can_summarize_topics_group",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.RESTRICT,
related_name="+",
to="zerver.usergroup",
),
),
]

View File

@@ -0,0 +1,35 @@
# Generated by Django 5.0.10 on 2025-02-10 10:31
from django.db import migrations
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from django.db.migrations.state import StateApps
from django.db.models import OuterRef
def set_default_for_can_summarize_topics_group(
apps: StateApps, schema_editor: BaseDatabaseSchemaEditor
) -> None:
Realm = apps.get_model("zerver", "Realm")
NamedUserGroup = apps.get_model("zerver", "NamedUserGroup")
Realm.objects.filter(can_summarize_topics_group=None).update(
can_summarize_topics_group=NamedUserGroup.objects.filter(
name="role:everyone", realm=OuterRef("id"), is_system_group=True
).values("pk")
)
class Migration(migrations.Migration):
atomic = False
dependencies = [
("zerver", "0664_realm_can_summarize_topics_group"),
]
operations = [
migrations.RunPython(
set_default_for_can_summarize_topics_group,
elidable=True,
reverse_code=migrations.RunPython.noop,
)
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 5.0.10 on 2025-02-10 10:36
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("zerver", "0665_set_default_for_can_summarize_topics_group"),
]
operations = [
migrations.AlterField(
model_name="realm",
name="can_summarize_topics_group",
field=models.ForeignKey(
on_delete=django.db.models.deletion.RESTRICT,
related_name="+",
to="zerver.usergroup",
),
),
]

View File

@@ -293,6 +293,11 @@ class Realm(models.Model): # type: ignore[django-manager-missing] # django-stub
"UserGroup", on_delete=models.RESTRICT, related_name="+" "UserGroup", on_delete=models.RESTRICT, related_name="+"
) )
# UserGroup whose members are allowed to summarize topics.
can_summarize_topics_group = models.ForeignKey(
"UserGroup", on_delete=models.RESTRICT, related_name="+"
)
# UserGroup whose members are allowed to create invite link. # UserGroup whose members are allowed to create invite link.
create_multiuse_invite_group = models.ForeignKey( create_multiuse_invite_group = models.ForeignKey(
"UserGroup", on_delete=models.RESTRICT, related_name="+" "UserGroup", on_delete=models.RESTRICT, related_name="+"
@@ -790,6 +795,13 @@ class Realm(models.Model): # type: ignore[django-manager-missing] # django-stub
allow_everyone_group=True, allow_everyone_group=True,
default_group_name=SystemGroups.EVERYONE, default_group_name=SystemGroups.EVERYONE,
), ),
can_summarize_topics_group=GroupPermissionSetting(
require_system_group=False,
allow_internet_group=False,
allow_nobody_group=True,
allow_everyone_group=True,
default_group_name=SystemGroups.EVERYONE,
),
direct_message_initiator_group=GroupPermissionSetting( direct_message_initiator_group=GroupPermissionSetting(
require_system_group=False, require_system_group=False,
allow_internet_group=False, allow_internet_group=False,
@@ -1203,6 +1215,8 @@ def get_realm_with_settings(realm_id: int) -> Realm:
"can_move_messages_between_channels_group__named_user_group", "can_move_messages_between_channels_group__named_user_group",
"can_move_messages_between_topics_group", "can_move_messages_between_topics_group",
"can_move_messages_between_topics_group__named_user_group", "can_move_messages_between_topics_group__named_user_group",
"can_summarize_topics_group",
"can_summarize_topics_group__named_user_group",
"direct_message_initiator_group", "direct_message_initiator_group",
"direct_message_initiator_group__named_user_group", "direct_message_initiator_group__named_user_group",
"direct_message_permission_group", "direct_message_permission_group",

View File

@@ -873,6 +873,9 @@ class UserProfile(AbstractBaseUser, PermissionsMixin, UserBaseSettings):
def can_delete_own_message(self) -> bool: def can_delete_own_message(self) -> bool:
return self.has_permission("can_delete_own_message_group") return self.has_permission("can_delete_own_message_group")
def can_summarize_topics(self) -> bool:
return self.has_permission("can_summarize_topics_group")
def can_access_public_streams(self) -> bool: def can_access_public_streams(self) -> bool:
return not (self.is_guest or self.realm.is_zephyr_mirror_realm) return not (self.is_guest or self.realm.is_zephyr_mirror_realm)

View File

@@ -4647,6 +4647,14 @@ paths:
create and edit user groups. create and edit user groups.
[calc-full-member]: /api/roles-and-permissions#determining-if-a-user-is-a-full-member [calc-full-member]: /api/roles-and-permissions#determining-if-a-user-is-a-full-member
can_summarize_topics_group:
allOf:
- $ref: "#/components/schemas/GroupSettingValue"
- description: |
A [group-setting value](/api/group-setting-values) defining the
set of users who are allowed to use AI summarization.
**Changes**: New in Zulip 10.0 (feature level 350).
create_multiuse_invite_group: create_multiuse_invite_group:
allOf: allOf:
- $ref: "#/components/schemas/GroupSettingValue" - $ref: "#/components/schemas/GroupSettingValue"
@@ -17662,6 +17670,14 @@ paths:
to be of type integer and did not accept anonymous user groups. to be of type integer and did not accept anonymous user groups.
New in Zulip 8.0 (feature level 225). New in Zulip 8.0 (feature level 225).
realm_can_summarize_topics_group:
allOf:
- $ref: "#/components/schemas/GroupSettingValue"
- description: |
A [group-setting value](/api/group-setting-values) defining the
set of users who are allowed to use AI summarization.
**Changes**: New in Zulip 10.0 (feature level 350).
zulip_plan_is_not_limited: zulip_plan_is_not_limited:
type: boolean type: boolean
description: | description: |

View File

@@ -142,6 +142,7 @@ class HomeTest(ZulipTestCase):
"realm_can_manage_all_groups", "realm_can_manage_all_groups",
"realm_can_move_messages_between_channels_group", "realm_can_move_messages_between_channels_group",
"realm_can_move_messages_between_topics_group", "realm_can_move_messages_between_topics_group",
"realm_can_summarize_topics_group",
"realm_create_multiuse_invite_group", "realm_create_multiuse_invite_group",
"realm_create_private_stream_policy", "realm_create_private_stream_policy",
"realm_create_public_stream_policy", "realm_create_public_stream_policy",

View File

@@ -7,7 +7,11 @@ from django.conf import settings
from typing_extensions import override from typing_extensions import override
from analytics.models import UserCount from analytics.models import UserCount
from zerver.actions.realm_settings import do_change_realm_permission_group_setting
from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_classes import ZulipTestCase
from zerver.models import NamedUserGroup
from zerver.models.groups import SystemGroups
from zerver.models.realms import get_realm
warnings.filterwarnings("ignore", category=UserWarning, module="pydantic") warnings.filterwarnings("ignore", category=UserWarning, module="pydantic")
warnings.filterwarnings("ignore", category=DeprecationWarning, module="pydantic") warnings.filterwarnings("ignore", category=DeprecationWarning, module="pydantic")
@@ -119,3 +123,82 @@ class MessagesSummaryTestCase(ZulipTestCase):
): ):
response = self.client_get("/json/messages/summary") response = self.client_get("/json/messages/summary")
self.assert_json_error_contains(response, "Reached monthly limit for AI credits.") self.assert_json_error_contains(response, "Reached monthly limit for AI credits.")
def test_permission_to_summarize_message_in_topics(self) -> None:
narrow = orjson.dumps([["channel", self.channel_name], ["topic", self.topic_name]]).decode()
realm = get_realm("zulip")
moderators_group = NamedUserGroup.objects.get(
name=SystemGroups.MODERATORS, realm=realm, is_system_group=True
)
do_change_realm_permission_group_setting(
realm,
"can_summarize_topics_group",
moderators_group,
acting_user=None,
)
# In this code path, we test using the fixtures.
with open(LLM_FIXTURES_FILE, "rb") as f:
fixture_data = orjson.loads(f.read())
def check_message_summary_permission(user: str, expect_fail: bool = False) -> None:
self.login(user)
with (
self.settings(
TOPIC_SUMMARIZATION_MODEL="groq/llama-3.3-70b-versatile",
TOPIC_SUMMARIZATION_API_KEY="test",
),
mock.patch("litellm.completion", return_value=fixture_data["response"]),
):
result = self.client_get("/json/messages/summary", dict(narrow=narrow))
if expect_fail:
self.assert_json_error(result, "Insufficient permission")
else:
self.assert_json_success(result)
check_message_summary_permission("hamlet", expect_fail=True)
check_message_summary_permission("shiva")
nobody_group = NamedUserGroup.objects.get(
name=SystemGroups.NOBODY, realm=realm, is_system_group=True
)
do_change_realm_permission_group_setting(
realm,
"can_summarize_topics_group",
nobody_group,
acting_user=None,
)
check_message_summary_permission("desdemona", expect_fail=True)
hamletcharacters_group = NamedUserGroup.objects.get(name="hamletcharacters", realm=realm)
do_change_realm_permission_group_setting(
realm,
"can_summarize_topics_group",
hamletcharacters_group,
acting_user=None,
)
check_message_summary_permission("desdemona", expect_fail=True)
check_message_summary_permission("othello", expect_fail=True)
check_message_summary_permission("hamlet")
check_message_summary_permission("cordelia")
setting_group = self.create_or_update_anonymous_group_for_setting(
[self.example_user("othello")], [moderators_group]
)
do_change_realm_permission_group_setting(
realm,
"can_summarize_topics_group",
setting_group,
acting_user=None,
)
check_message_summary_permission("cordelia", expect_fail=True)
check_message_summary_permission("hamlet", expect_fail=True)
check_message_summary_permission("othello")
check_message_summary_permission("shiva")
check_message_summary_permission("desdemona")

View File

@@ -27,8 +27,8 @@ def get_messages_summary(
if settings.TOPIC_SUMMARIZATION_MODEL is None: # nocoverage if settings.TOPIC_SUMMARIZATION_MODEL is None: # nocoverage
raise JsonableError(_("AI features are not enabled on this server.")) raise JsonableError(_("AI features are not enabled on this server."))
if not (user_profile.is_moderator or user_profile.is_realm_admin): # nocoverage if not user_profile.can_summarize_topics():
return json_success(request, {"summary": "Feature limited to moderators for now."}) raise JsonableError(_("Insufficient permission"))
if settings.MAX_PER_USER_MONTHLY_AI_COST is not None: if settings.MAX_PER_USER_MONTHLY_AI_COST is not None:
used_credits = COUNT_STATS["ai_credit_usage::day"].current_month_accumulated_count_for_user( used_credits = COUNT_STATS["ai_credit_usage::day"].current_month_accumulated_count_for_user(

View File

@@ -136,6 +136,7 @@ def update_realm(
can_manage_all_groups: Json[GroupSettingChangeRequest] | None = None, can_manage_all_groups: Json[GroupSettingChangeRequest] | None = None,
can_move_messages_between_channels_group: Json[GroupSettingChangeRequest] | None = None, can_move_messages_between_channels_group: Json[GroupSettingChangeRequest] | None = None,
can_move_messages_between_topics_group: Json[GroupSettingChangeRequest] | None = None, can_move_messages_between_topics_group: Json[GroupSettingChangeRequest] | None = None,
can_summarize_topics_group: Json[GroupSettingChangeRequest] | None = None,
direct_message_initiator_group: Json[GroupSettingChangeRequest] | None = None, direct_message_initiator_group: Json[GroupSettingChangeRequest] | None = None,
direct_message_permission_group: Json[GroupSettingChangeRequest] | None = None, direct_message_permission_group: Json[GroupSettingChangeRequest] | None = None,
wildcard_mention_policy: Json[WildcardMentionPolicyEnum] | None = None, wildcard_mention_policy: Json[WildcardMentionPolicyEnum] | None = None,