openvibers/openvibe.contracts Commits

chat.vibe.ask: a first-party service asks Vibe on a person's behalf (Bot's Vibe by voice), kept in their own Vibe chats; chat.vibe-ask-request/result@1 (0.214.0) (#215)

OP OpenVibers committed 19 days ago · cc3097c8d5 · 15 files, +484 −3

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 3429a84e..df575583 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -14,6 +14,11 @@ the notes were in the tag and commit messages (`git tag -n1`).
`url` of the Vibe chat that keeps it.
- `config` carries `voice` (the owner turned it on for this machine) and `call_word`.
Fixtures for each, and two invalid ones.
++- **`chat.vibe.ask`** (first-party, sensitive, owned by `chat`): a first-party service asks Vibe on a person's behalf
++ (`POST /internal/chat/vibe/ask`): Bot's Vibe by voice, with the machine's owner as `subject`. Chat keeps the question
++ and the answer in that person's own Vibe chats (one chat per source, "Voice on <machine>") and counts the answer
++ against their daily allowance. Schemas `chat.vibe-ask-request@1` (subject, text, source { kind voice, id, name },
++ lang, idempotency_key) and `chat.vibe-ask-result@1` (state answered | refused, text, url, chat, code), with fixtures.
- **Quiet hours for personal agents** (consumer plan step 4: "recurring work … with quiet hours and a budget").
`actor.agent-manage-request@1` and `-result@1` carry `quiet_from` / `quiet_to` (HH:MM UTC, both or neither; the window
may cross midnight): no scheduled run starts inside it, an hourly run waits until it ends, and a run the owner starts
ddiff --git a/contracts/catalog.json b/contracts/catalog.json
iindex 48b6a8e9..f579f7c1 100644
---- a/contracts/catalog.json
++++ b/contracts/catalog.json
@@@ -5078,6 +5078,24 @@
"compatibility": "backward",
"status": "active"
},
++ {
++ "id": "chat.vibe-ask-request",
++ "version": "1.0.0",
++ "owner": "chat",
++ "schema": "chat/vibe-ask-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "chat.vibe-ask-result",
++ "version": "1.0.0",
++ "owner": "chat",
++ "schema": "chat/vibe-ask-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
{
"id": "live.moderation.action",
"version": "1.0.0",
ddiff --git a/contracts/chat/vibe-ask-request.v1.json b/contracts/chat/vibe-ask-request.v1.json
nnew file mode 100644
iindex 00000000..2d90640e
---- /dev/null
++++ b/contracts/chat/vibe-ask-request.v1.json
@@@ -0,0 +1,60 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/chat/vibe-ask-request.v1.json",
++ "title": "ChatVibeAskRequest",
++ "description": "chat.vibe-ask-request@1: a first-party service asks Vibe, OpenVibe's assistant, on a person's behalf (Vibe by voice: OpenVibe.Bot passes what the person said after their machine's call word). Chat keeps it in the person's own Vibe chats, one chat per source (\"Voice on <machine>\"), and answers with Vibe's reply, which counts against the person's daily allowance like any of their Vibe answers. POST /internal/chat/vibe/ask, capability chat.vibe.ask, loopback only.",
++ "type": "object",
++ "required": [
++ "subject",
++ "text",
++ "source"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "subject": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The person Vibe answers, as the calling service vouches (Bot: the machine's owner, with voice on for that machine)."
++ },
++ "text": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 2000,
++ "description": "What they asked."
++ },
++ "source": {
++ "type": "object",
++ "required": [
++ "kind",
++ "id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "kind": {
++ "enum": [
++ "voice"
++ ]
++ },
++ "id": {
++ "type": "string",
++ "pattern": "^[A-Za-z0-9_.:-]{1,64}$",
++ "description": "The source's stable id (the machine's device id): one chat per source."
++ },
++ "name": {
++ "type": "string",
++ "maxLength": 80,
++ "description": "What to call it in the chat's name (the machine's name)."
++ }
++ }
++ },
++ "lang": {
++ "type": "string",
++ "pattern": "^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8}){0,2}$"
++ },
++ "idempotency_key": {
++ "type": "string",
++ "pattern": "^[A-Za-z0-9_.:-]{1,128}$",
++ "description": "The caller's id for this ask (Bot: device id and the voice_ask id): asked again, the same answer, once."
++ }
++ }
++}
ddiff --git a/contracts/chat/vibe-ask-result.v1.json b/contracts/chat/vibe-ask-result.v1.json
nnew file mode 100644
iindex 00000000..80a45fc5
---- /dev/null
++++ b/contracts/chat/vibe-ask-result.v1.json
@@@ -0,0 +1,38 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/chat/vibe-ask-result.v1.json",
++ "title": "ChatVibeAskResult",
++ "description": "chat.vibe-ask-result@1: Vibe's answer to a chat.vibe-ask-request@1. `answered` carries the reply (plain text, Markdown stripped, for speech); `refused` says why nothing was asked (Vibe resting, the person's daily allowance spent). `url` is the Vibe chat that keeps the question and the answer.",
++ "type": "object",
++ "required": [
++ "state",
++ "text"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "state": {
++ "enum": [
++ "answered",
++ "refused"
++ ]
++ },
++ "text": {
++ "type": "string",
++ "maxLength": 4000
++ },
++ "url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https://"
++ },
++ "chat": {
++ "type": "string",
++ "pattern": "^v-[0-9a-f]{12}$",
++ "description": "The Vibe chat's slug."
++ },
++ "code": {
++ "type": "string",
++ "description": "Why it was refused (chat.vibe.off, chat.vibe.allowance)."
++ }
++ }
++}
ddiff --git a/fixtures/chat.vibe-ask-request/invalid/no-source.json b/fixtures/chat.vibe-ask-request/invalid/no-source.json
nnew file mode 100644
iindex 00000000..6199d9c0
---- /dev/null
++++ b/fixtures/chat.vibe-ask-request/invalid/no-source.json
@@@ -0,0 +1,4 @@
++{
++ "subject": "usr_01JABCDEFGHJKMNPQRSTVWXYZ0",
++ "text": "hello"
++}
ddiff --git a/fixtures/chat.vibe-ask-request/valid/voice.json b/fixtures/chat.vibe-ask-request/valid/voice.json
nnew file mode 100644
iindex 00000000..e630c771
---- /dev/null
++++ b/fixtures/chat.vibe-ask-request/valid/voice.json
@@@ -0,0 +1,11 @@
++{
++ "subject": "usr_01JABCDEFGHJKMNPQRSTVWXYZ0",
++ "text": "what is the weather like this evening",
++ "source": {
++ "kind": "voice",
++ "id": "dev_01JABCDEFGHJKMNPQRSTVWXYZ0",
++ "name": "desk-pc"
++ },
++ "lang": "en-US",
++ "idempotency_key": "dev_01JABCDEFGHJKMNPQRSTVWXYZ0:va-001"
++}
ddiff --git a/fixtures/chat.vibe-ask-result/invalid/bad-state.json b/fixtures/chat.vibe-ask-result/invalid/bad-state.json
nnew file mode 100644
iindex 00000000..0a101ebb
---- /dev/null
++++ b/fixtures/chat.vibe-ask-result/invalid/bad-state.json
@@@ -0,0 +1,4 @@
++{
++ "state": "maybe",
++ "text": "x"
++}
ddiff --git a/fixtures/chat.vibe-ask-result/valid/answered.json b/fixtures/chat.vibe-ask-result/valid/answered.json
nnew file mode 100644
iindex 00000000..59d5f338
---- /dev/null
++++ b/fixtures/chat.vibe-ask-result/valid/answered.json
@@@ -0,0 +1,6 @@
++{
++ "state": "answered",
++ "text": "Clear skies after six, around twelve degrees.",
++ "url": "https://openvibe.chat/vibe/v-0123456789ab",
++ "chat": "v-0123456789ab"
++}
ddiff --git a/fixtures/chat.vibe-ask-result/valid/refused.json b/fixtures/chat.vibe-ask-result/valid/refused.json
nnew file mode 100644
iindex 00000000..5c6eed77
---- /dev/null
++++ b/fixtures/chat.vibe-ask-result/valid/refused.json
@@@ -0,0 +1,5 @@
++{
++ "state": "refused",
++ "text": "You have used today's free answers.",
++ "code": "chat.vibe.allowance"
++}
ddiff --git a/generated/json-schema/bundle.json b/generated/json-schema/bundle.json
iindex 072327aa..e79f5599 100644
---- a/generated/json-schema/bundle.json
++++ b/generated/json-schema/bundle.json
@@@ -56367,6 +56367,114 @@
]
}
},
++ "chat.vibe-ask-request": {
++ "version": "1.0.0",
++ "owner": "chat",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/chat/vibe-ask-request.v1.json",
++ "title": "ChatVibeAskRequest",
++ "description": "chat.vibe-ask-request@1: a first-party service asks Vibe, OpenVibe's assistant, on a person's behalf (Vibe by voice: OpenVibe.Bot passes what the person said after their machine's call word). Chat keeps it in the person's own Vibe chats, one chat per source (\"Voice on <machine>\"), and answers with Vibe's reply, which counts against the person's daily allowance like any of their Vibe answers. POST /internal/chat/vibe/ask, capability chat.vibe.ask, loopback only.",
++ "type": "object",
++ "required": [
++ "subject",
++ "text",
++ "source"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "subject": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The person Vibe answers, as the calling service vouches (Bot: the machine's owner, with voice on for that machine)."
++ },
++ "text": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 2000,
++ "description": "What they asked."
++ },
++ "source": {
++ "type": "object",
++ "required": [
++ "kind",
++ "id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "kind": {
++ "enum": [
++ "voice"
++ ]
++ },
++ "id": {
++ "type": "string",
++ "pattern": "^[A-Za-z0-9_.:-]{1,64}$",
++ "description": "The source's stable id (the machine's device id): one chat per source."
++ },
++ "name": {
++ "type": "string",
++ "maxLength": 80,
++ "description": "What to call it in the chat's name (the machine's name)."
++ }
++ }
++ },
++ "lang": {
++ "type": "string",
++ "pattern": "^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8}){0,2}$"
++ },
++ "idempotency_key": {
++ "type": "string",
++ "pattern": "^[A-Za-z0-9_.:-]{1,128}$",
++ "description": "The caller's id for this ask (Bot: device id and the voice_ask id): asked again, the same answer, once."
++ }
++ }
++ }
++ },
++ "chat.vibe-ask-result": {
++ "version": "1.0.0",
++ "owner": "chat",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/chat/vibe-ask-result.v1.json",
++ "title": "ChatVibeAskResult",
++ "description": "chat.vibe-ask-result@1: Vibe's answer to a chat.vibe-ask-request@1. `answered` carries the reply (plain text, Markdown stripped, for speech); `refused` says why nothing was asked (Vibe resting, the person's daily allowance spent). `url` is the Vibe chat that keeps the question and the answer.",
++ "type": "object",
++ "required": [
++ "state",
++ "text"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "state": {
++ "enum": [
++ "answered",
++ "refused"
++ ]
++ },
++ "text": {
++ "type": "string",
++ "maxLength": 4000
++ },
++ "url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https://"
++ },
++ "chat": {
++ "type": "string",
++ "pattern": "^v-[0-9a-f]{12}$",
++ "description": "The Vibe chat's slug."
++ },
++ "code": {
++ "type": "string",
++ "description": "Why it was refused (chat.vibe.off, chat.vibe.allowance)."
++ }
++ }
++ }
++ },
"live.moderation.action": {
"version": "1.0.0",
"owner": "live",
ddiff --git a/generated/openapi/chat.json b/generated/openapi/chat.json
iindex 13964128..3c32de3d 100644
---- a/generated/openapi/chat.json
++++ b/generated/openapi/chat.json
@@@ -4083,6 +4083,62 @@
}
}
},
++ "/internal/chat/vibe/ask": {
++ "post": {
++ "operationId": "post_internal_chat_vibe_ask",
++ "summary": "Ask Vibe, OpenVibe's assistant, on a person's behalf and get its answer: the question and the answer are kept in that person's own Vibe chats (one per source) and count against their daily allowance.",
++ "description": "**chat.vibe.ask** (first-party): Ask Vibe, OpenVibe's assistant, on a person's behalf and get its answer: the question and the answer are kept in that person's own Vibe chats (one per source) and count against their daily allowance. For OpenVibe.Bot's Vibe by voice (a machine's owner turned voice on; Bot vouches for the owner). Loopback only, first-party service token; never granted to an app or delegated to an agent.",
++ "tags": [
++ "chat.vibe"
++ ],
++ "x-openvibe-capabilities": [
++ "chat.vibe.ask"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "internal-write",
++ "security": [
++ {
++ "openvibeToken": [
++ "chat.vibe.ask"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/chat.vibe-ask-request.v1"
++ }
++ }
++ }
++ },
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/chat.vibe-ask-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
"/internal/live/calls": {
"post": {
"operationId": "post_internal_live_calls",
@@@ -8153,6 +8209,100 @@
}
]
},
++ "chat.vibe-ask-request.v1": {
++ "title": "ChatVibeAskRequest",
++ "description": "chat.vibe-ask-request@1: a first-party service asks Vibe, OpenVibe's assistant, on a person's behalf (Vibe by voice: OpenVibe.Bot passes what the person said after their machine's call word). Chat keeps it in the person's own Vibe chats, one chat per source (\"Voice on <machine>\"), and answers with Vibe's reply, which counts against the person's daily allowance like any of their Vibe answers. POST /internal/chat/vibe/ask, capability chat.vibe.ask, loopback only.",
++ "type": "object",
++ "required": [
++ "subject",
++ "text",
++ "source"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "subject": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The person Vibe answers, as the calling service vouches (Bot: the machine's owner, with voice on for that machine)."
++ },
++ "text": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 2000,
++ "description": "What they asked."
++ },
++ "source": {
++ "type": "object",
++ "required": [
++ "kind",
++ "id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "kind": {
++ "enum": [
++ "voice"
++ ]
++ },
++ "id": {
++ "type": "string",
++ "pattern": "^[A-Za-z0-9_.:-]{1,64}$",
++ "description": "The source's stable id (the machine's device id): one chat per source."
++ },
++ "name": {
++ "type": "string",
++ "maxLength": 80,
++ "description": "What to call it in the chat's name (the machine's name)."
++ }
++ }
++ },
++ "lang": {
++ "type": "string",
++ "pattern": "^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8}){0,2}$"
++ },
++ "idempotency_key": {
++ "type": "string",
++ "pattern": "^[A-Za-z0-9_.:-]{1,128}$",
++ "description": "The caller's id for this ask (Bot: device id and the voice_ask id): asked again, the same answer, once."
++ }
++ }
++ },
++ "chat.vibe-ask-result.v1": {
++ "title": "ChatVibeAskResult",
++ "description": "chat.vibe-ask-result@1: Vibe's answer to a chat.vibe-ask-request@1. `answered` carries the reply (plain text, Markdown stripped, for speech); `refused` says why nothing was asked (Vibe resting, the person's daily allowance spent). `url` is the Vibe chat that keeps the question and the answer.",
++ "type": "object",
++ "required": [
++ "state",
++ "text"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "state": {
++ "enum": [
++ "answered",
++ "refused"
++ ]
++ },
++ "text": {
++ "type": "string",
++ "maxLength": 4000
++ },
++ "url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https://"
++ },
++ "chat": {
++ "type": "string",
++ "pattern": "^v-[0-9a-f]{12}$",
++ "description": "The Vibe chat's slug."
++ },
++ "code": {
++ "type": "string",
++ "description": "Why it was refused (chat.vibe.off, chat.vibe.allowance)."
++ }
++ }
++ },
"common.owned-resource-control-request.v1": {
"title": "OwnedResourceControlRequest",
"description": "common.owned-resource-control-request@1 (ADR-048 amendment 2026-10-15, step 8 phase 2): one control operation on a person-owned resource. Robots, chat rooms, restream definitions and pastes belong to a person, not to a project, so they carry no OVRN and the project-scoped common.resource-control-request@1 cannot name them. OpenVibe.Services sends this to the owning authority's POST /api/v1/resources/control (the same route, told apart by `owned`), acting for the person in on_behalf_of. owned names the resource by its index summary's kind and id. owner is the person it belongs to: the authority refuses unless its own record says the same (Services also offers actions only on the viewer's own resources). A sensitive action (deleting, physical control) asked by an agent is refused with confirmation_required until the owner approves, as for project resources. idempotency_key makes a retry safe: the authority answers a repeated key from the same caller with the first result, and refuses the key reused with another request. Answered with common.resource-control-result@1 (without resource: there is no OVRN). The actions each authority takes are in its <service>.resource.control capability; any other action is refused unsupported.",
ddiff --git a/generated/openapi/index.json b/generated/openapi/index.json
iindex ece880d7..053f952a 100644
---- a/generated/openapi/index.json
++++ b/generated/openapi/index.json
@@@ -40,8 +40,8 @@
"service": "chat",
"name": "OpenVibe.Chat",
"origin": "https://openvibe.chat",
-- "operations": 52,
-- "capabilities": 17
++ "operations": 53,
++ "capabilities": 18
},
{
"service": "codes",
ddiff --git a/generated/typescript/index.d.ts b/generated/typescript/index.d.ts
iindex 566665d1..8e1ab5e2 100644
---- a/generated/typescript/index.d.ts
++++ b/generated/typescript/index.d.ts
@@@ -190974,6 +190974,55 @@ export interface ChatTicketConversation {
};
}
++/** [email protected] (owner: chat) */
++/**
++ * chat.vibe-ask-request@1: a first-party service asks Vibe, OpenVibe's assistant, on a person's behalf (Vibe by voice: OpenVibe.Bot passes what the person said after their machine's call word). Chat keeps it in the person's own Vibe chats, one chat per source ("Voice on <machine>"), and answers with Vibe's reply, which counts against the person's daily allowance like any of their Vibe answers. POST /internal/chat/vibe/ask, capability chat.vibe.ask, loopback only.
++ */
++export interface ChatVibeAskRequest {
++ /**
++ * The person Vibe answers, as the calling service vouches (Bot: the machine's owner, with voice on for that machine).
++ */
++ subject: string;
++ /**
++ * What they asked.
++ */
++ text: string;
++ source: {
++ kind: "voice";
++ /**
++ * The source's stable id (the machine's device id): one chat per source.
++ */
++ id: string;
++ /**
++ * What to call it in the chat's name (the machine's name).
++ */
++ name?: string;
++ };
++ lang?: string;
++ /**
++ * The caller's id for this ask (Bot: device id and the voice_ask id): asked again, the same answer, once.
++ */
++ idempotency_key?: string;
++}
++
++/** [email protected] (owner: chat) */
++/**
++ * chat.vibe-ask-result@1: Vibe's answer to a chat.vibe-ask-request@1. `answered` carries the reply (plain text, Markdown stripped, for speech); `refused` says why nothing was asked (Vibe resting, the person's daily allowance spent). `url` is the Vibe chat that keeps the question and the answer.
++ */
++export interface ChatVibeAskResult {
++ state: "answered" | "refused";
++ text: string;
++ url?: string;
++ /**
++ * The Vibe chat's slug.
++ */
++ chat?: string;
++ /**
++ * Why it was refused (chat.vibe.off, chat.vibe.allowance).
++ */
++ code?: string;
++}
++
/** [email protected] (owner: live) */
/**
* live.moderation.action v1 (OpenVibe.Live server/db/database.js logModerationAction; ADR-022). A staff or channel moderator action taken on OpenVibe.Live outside chat: site and global bans, IP bans, message deletes and purges from Live's admin panel, a stream force-ended, relay users hidden, channel moderators added or removed. Not a person tidying their own messages or configuring their own channel. Written to Live's outbox in the transaction that records the action. OpenVibe.Network keeps it in the moderation audit log. Envelope: subject { type: moderation_action, id: <action_id> }, visibility internal, actor the acting person when known. details is free-form per action (never secrets or message text beyond what moderators saw).
ddiff --git a/manifests/capabilities/chat.vibe.ask.json b/manifests/capabilities/chat.vibe.ask.json
nnew file mode 100644
iindex 00000000..17da3ca0
---- /dev/null
++++ b/manifests/capabilities/chat.vibe.ask.json
@@@ -0,0 +1,22 @@
++{
++ "id": "chat.vibe.ask",
++ "version": "1.0",
++ "owner": "chat",
++ "status": "active",
++ "visibility": "first-party",
++ "sensitive": true,
++ "description": "Ask Vibe, OpenVibe's assistant, on a person's behalf and get its answer: the question and the answer are kept in that person's own Vibe chats (one per source) and count against their daily allowance. For OpenVibe.Bot's Vibe by voice (a machine's owner turned voice on; Bot vouches for the owner). Loopback only, first-party service token; never granted to an app or delegated to an agent.",
++ "permissions": [
++ "chat:write"
++ ],
++ "resourceConstraints": [
++ "none"
++ ],
++ "quotaClass": "internal-write",
++ "events": [],
++ "implementedBy": [
++ "POST /internal/chat/vibe/ask"
++ ],
++ "inputSchema": "chat.vibe-ask-request@1",
++ "outputSchema": "chat.vibe-ask-result@1"
++}
ddiff --git a/manifests/services/chat.json b/manifests/services/chat.json
iindex 41fb5f98..5a444c32 100644
---- a/manifests/services/chat.json
++++ b/manifests/services/chat.json
@@@ -30,7 +30,8 @@
"chat.resource.control",
"chat.inbox.read",
"chat.inbox.reply",
-- "chat.inbox.manage"
++ "chat.inbox.manage",
++ "chat.vibe.ask"
],
"eventsProduced": [
"chat.message.created",