PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / 2.5.0
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution v2.5.0
2.5.0 2.4.0 2.3.0 2.2.5 2.2.0 2.1.2 2.1.1 trunk 1.10.0 1.10.01 1.10.02 1.5.0 1.5.01 1.5.02 1.5.1 1.5.10 1.5.20 1.5.21 1.5.22 1.5.23 1.5.24 1.5.25 1.6.0 1.7.0 1.7.1 All 34 releases
fluent-booking / app / Modules / MCP / Prompts / BookingPrompts.php

BookingPrompts.php in Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution 2.5.0, at app/Modules/MCP/Prompts/BookingPrompts.php

289 lines 13.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentBooking\App\Modules\MCP\Prompts;
4
5 use FluentBooking\App\Modules\MCP\Support\MCPHelper;
6 use FluentBooking\App\Modules\MCP\Support\PermissionGate;
7 use FluentBooking\Framework\Support\Arr;
8
9 defined('ABSPATH') || exit;
10
11 /**
12 * MCP prompts: step-by-step procedures the operator can run by name.
13 *
14 * Clients fetch a prompt's body only when it runs, so long guidance lives here
15 * rather than in tool descriptions. These double as the eval fixtures in §15
16 * of the spec.
17 *
18 * @since 2.2.6
19 */
20 class BookingPrompts
21 {
22 public static function definitions()
23 {
24 return [
25 'fluent-booking/daily-briefing' => [
26 'label' => __('Daily briefing', 'fluent-booking'),
27 'description' => __('Summarise a day\'s schedule: what is booked, what needs a decision, and what changed.', 'fluent-booking'),
28 'input_schema' => [
29 'type' => 'object',
30 'properties' => [
31 'date' => [
32 'type' => 'string',
33 'description' => __('Y-m-d. Defaults to today.', 'fluent-booking'),
34 ],
35 'host_id' => [
36 'type' => 'integer',
37 'description' => __('Brief for one host only.', 'fluent-booking'),
38 ],
39 'timezone' => [
40 'type' => 'string',
41 'description' => __('IANA zone to report times in.', 'fluent-booking'),
42 ],
43 ],
44 ],
45 'is_prompt' => true,
46 'permission_callback' => [PermissionGate::class, 'readGate'],
47 'execute_callback' => [self::class, 'dailyBriefing'],
48 ],
49
50 'fluent-booking/troubleshoot-event' => [
51 'label' => __('Troubleshoot an event type', 'fluent-booking'),
52 'description' => __('Work out why an event type is showing no slots, or the wrong ones, and say what to change.', 'fluent-booking'),
53 'input_schema' => [
54 'type' => 'object',
55 'properties' => [
56 'event_id' => [
57 'type' => 'integer',
58 'description' => __('The event type to investigate.', 'fluent-booking'),
59 ],
60 'complaint' => [
61 'type' => 'string',
62 'description' => __('What the operator or attendee actually reported, in their own words.', 'fluent-booking'),
63 ],
64 'from' => ['type' => 'string'],
65 'to' => ['type' => 'string'],
66 ],
67 'required' => ['event_id'],
68 ],
69 'is_prompt' => true,
70 'permission_callback' => [PermissionGate::class, 'readGate'],
71 'execute_callback' => [self::class, 'troubleshootEvent'],
72 ],
73
74 'fluent-booking/weekly-report' => [
75 'label' => __('Weekly report', 'fluent-booking'),
76 'description' => __('A week\'s booking numbers, with the comparison and the caveats that make them trustworthy.', 'fluent-booking'),
77 'input_schema' => [
78 'type' => 'object',
79 'properties' => [
80 'from' => [
81 'type' => 'string',
82 'description' => __('Y-m-d, start of the period. Defaults to the last 7 days.', 'fluent-booking'),
83 ],
84 'to' => ['type' => 'string'],
85 ],
86 ],
87 'is_prompt' => true,
88 'permission_callback' => [PermissionGate::class, 'readGate'],
89 'execute_callback' => [self::class, 'weeklyReport'],
90 ],
91 ];
92 }
93
94 /**
95 * @param array $params
96 * @return array
97 */
98 public static function dailyBriefing($params = [])
99 {
100 $date = self::date(Arr::get($params, 'date'), gmdate('Y-m-d'));
101 $hostId = absint(Arr::get($params, 'host_id'));
102 $timezone = sanitize_text_field(Arr::get($params, 'timezone', ''));
103
104 $filters = "period: \"all\", from: \"{$date}\", to: \"{$date}\"";
105
106 if ($hostId) {
107 $filters .= ", host_id: {$hostId}";
108 }
109
110 if ($timezone) {
111 $filters .= ", timezone: \"{$timezone}\"";
112 }
113
114 $text = self::lines([
115 "Brief the operator on {$date}.",
116 '',
117 'Do this:',
118 '',
119 '1. Call `fluent-booking/get-booking-context` first. It gives you the site timezone, the current time, and which bookings this account may see. Every time you report below must carry a stated zone.',
120 "2. Call `fluent-booking/list-bookings` with {$filters} for the day's schedule.",
121 '3. Call it again with `status: ["pending"]` and no date range. Requests waiting on a decision are the thing most likely to be forgotten, and they are not necessarily on today.',
122 '',
123 'Then write the briefing:',
124 '',
125 '- **The day.** Each booking in start order: time (with zone), duration, attendee, event type, host. Say plainly if the day is empty.',
126 '- **Needs a decision.** Pending requests, oldest first, with how long each has been waiting.',
127 '- **Worth noticing.** Back-to-back bookings with no gap; anything cancelled or rescheduled in the last 24 hours; a host carrying noticeably more than the others.',
128 '',
129 'Rules:',
130 '',
131 '- If `meta.scope` is `own_calendars`, say so in one line at the top. The operator is seeing their own calendars, not the site.',
132 '- If `meta.pii_masked` is true, do not guess at the masked addresses.',
133 '- Report what the tools returned. If something looks wrong, say it looks wrong and name the tool that said it — do not quietly correct it.',
134 ]);
135
136 return self::prompt(
137 /* translators: %s: the date being briefed */
138 sprintf(__('Daily briefing for %s', 'fluent-booking'), $date),
139 $text
140 );
141 }
142
143 /**
144 * @param array $params
145 * @return array
146 */
147 public static function troubleshootEvent($params = [])
148 {
149 $eventId = absint(Arr::get($params, 'event_id'));
150 $complaint = sanitize_textarea_field(Arr::get($params, 'complaint', ''));
151 $from = self::date(Arr::get($params, 'from'), gmdate('Y-m-d'));
152 $to = self::date(Arr::get($params, 'to'), gmdate('Y-m-d', strtotime('+13 days')));
153
154 $lines = [
155 "Find out why event type {$eventId} is not offering the slots someone expected, between {$from} and {$to}.",
156 ];
157
158 if ($complaint) {
159 $lines[] = '';
160 $lines[] = 'What was reported:';
161 $lines[] = '';
162 $lines[] = '> ' . $complaint;
163 }
164
165 $lines = array_merge($lines, [
166 '',
167 'Do this, in order:',
168 '',
169 "1. `fluent-booking/diagnose-availability` with `event_id: {$eventId}`, `from: \"{$from}\"`, `to: \"{$to}\"`. This is the tool built for exactly this question — start here, not with the slot list.",
170 "2. `fluent-booking/get-event-types` with `event_id: {$eventId}` for the configuration behind whichever checks failed.",
171 '3. `fluent-booking/get-available-slots` for the same window, to see what an attendee would actually be offered.',
172 '',
173 'Reading the diagnosis:',
174 '',
175 '- Every entry in `checks` has `passed` and `detail`. A failed check is a cause; a passed one is not evidence of health, only that it was not the problem.',
176 '- `empty_dates` attributes each blank day to a reason. `event_inactive`, `before_bookable_window`, `after_bookable_window`, `date_override_closed`, `no_weekly_hours`, `daily_cap_reached`, `fully_booked` and `minimum_notice` each point at a different setting.',
177 '- **`unexplained` means the diagnostic could not account for the day.** That is a real finding, not noise. Say so explicitly rather than passing over it — it usually means the slot engine and the stored settings disagree.',
178 '- If `truncated` is set on the slot response, the list was cut at `truncated_at`. Do not read the last date as the end of the calendar.',
179 '',
180 'Then answer:',
181 '',
182 '1. **What is wrong** — one sentence a non-technical operator understands.',
183 '2. **Why** — the specific setting, with its current value.',
184 '3. **What to change** — the setting and what to change it to. If the fix is not clear, say what you would need to know.',
185 '',
186 'If every check passes and slots are being offered, say the calendar looks correct and ask what the attendee actually saw — the complaint is then probably about a different event type, a different timezone, or a cache.',
187 ]);
188
189 return self::prompt(
190 /* translators: %d: the event type id */
191 sprintf(__('Troubleshoot event type %d', 'fluent-booking'), $eventId),
192 self::lines($lines)
193 );
194 }
195
196 /**
197 * @param array $params
198 * @return array
199 */
200 public static function weeklyReport($params = [])
201 {
202 $to = self::date(Arr::get($params, 'to'), gmdate('Y-m-d'));
203 $from = self::date(Arr::get($params, 'from'), gmdate('Y-m-d', strtotime($to . ' -6 days')));
204
205 $span = (int) floor((strtotime($to) - strtotime($from)) / DAY_IN_SECONDS) + 1;
206 $priorTo = gmdate('Y-m-d', strtotime($from . ' -1 day'));
207 $priorFrom = gmdate('Y-m-d', strtotime($priorTo . ' -' . ($span - 1) . ' days'));
208
209 $text = self::lines([
210 "Report on bookings from {$from} to {$to}.",
211 '',
212 'Do this:',
213 '',
214 "1. `fluent-booking/query-bookings` with `from: \"{$from}\"`, `to: \"{$to}\"`, `group_by: [\"status\"]`, `metrics: [\"count\", \"total_minutes\"]` — the headline numbers.",
215 "2. The same call for {$priorFrom} to {$priorTo}, the equal-length period before it, so the comparison is like for like.",
216 '3. `group_by: ["event"]` with `metrics: ["count", "cancellation_rate", "no_show_rate"]` — which event types are working.',
217 '4. `group_by: ["host"]` with `metrics: ["count", "total_minutes"]` — how the load is distributed.',
218 '5. `group_by: ["weekday"]` and `group_by: ["hour"]` — when people actually book. Pass a `timezone`, and report the offset that comes back in `meta.bucket_offset`.',
219 '',
220 'Then write the report:',
221 '',
222 '- **Headline.** Total bookings and hours, with the change against the prior period as both a number and a percentage.',
223 '- **By event type.** Ranked. Call out any cancellation or no-show rate that stands apart from the others.',
224 '- **By host.** Ranked by hours, not by count — a host doing four 90-minute sessions is busier than one doing six 15-minute calls.',
225 '- **Patterns.** The busiest weekday and hour, and anything that moved.',
226 '',
227 'Caveats you must carry through, because they change what the numbers mean:',
228 '',
229 '- Report `meta.scope`. On `own_calendars` these are the operator\'s own bookings, not the site\'s.',
230 '- If `meta.truncated` is set, more groups matched than were returned. Say so rather than presenting a partial ranking as complete.',
231 '- `distinct_attendees` cannot be summed across groups without double-counting; the response says so too.',
232 '- Weekday and hour buckets use one fixed offset for the whole range, so a range crossing a daylight-saving change can be an hour out at the far end.',
233 '- A percentage change off a small base is noise. Below about ten bookings, give the raw numbers and skip the percentage.',
234 ]);
235
236 return self::prompt(
237 /* translators: %1$s: start date, %2$s: end date */
238 sprintf(__('Booking report, %1$s to %2$s', 'fluent-booking'), $from, $to),
239 $text
240 );
241 }
242
243 /**
244 * The MCP prompt result shape: a description plus one or more messages.
245 *
246 * @param string $description
247 * @param string $text
248 *
249 * @return array
250 */
251 private static function prompt($description, $text)
252 {
253 return [
254 'description' => $description,
255 'messages' => [
256 [
257 'role' => 'user',
258 'content' => [
259 'type' => 'text',
260 'text' => $text,
261 ],
262 ],
263 ],
264 ];
265 }
266
267 /**
268 * @param array $lines
269 * @return string
270 */
271 private static function lines($lines)
272 {
273 return implode("\n", $lines);
274 }
275
276 /**
277 * @param mixed $value
278 * @param string $fallback
279 *
280 * @return string
281 */
282 private static function date($value, $fallback)
283 {
284 $value = sanitize_text_field((string) $value);
285
286 return MCPHelper::isRealDate($value) ? $value : $fallback;
287 }
288 }
289