PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / 2.4.0
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution v2.4.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.4.0, at app/Modules/MCP/Prompts/BookingPrompts.php

296 lines 14.0 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 — worked procedures the operator can invoke by name.
13 *
14 * Prompts are the right home for anything that would otherwise have been
15 * crammed into a tool description, because a prompt costs almost nothing until
16 * someone runs it: clients list its name, description and arguments, and fetch
17 * the body only on invocation. That makes them cheap in exactly the currency
18 * this design spends everywhere else — resident context.
19 *
20 * So each one here is a procedure rather than a paragraph: which tools to call,
21 * in what order, and what to do when they disagree. The three shipped are the
22 * three tasks that came up over and over while building the tools, and they
23 * double as the eval fixtures in §15 of the spec.
24 *
25 * @since 2.2.6
26 */
27 class BookingPrompts
28 {
29 public static function definitions()
30 {
31 return [
32 'fluent-booking/daily-briefing' => [
33 'label' => __('Daily briefing', 'fluent-booking'),
34 'description' => __('Summarise a day\'s schedule: what is booked, what needs a decision, and what changed.', 'fluent-booking'),
35 'input_schema' => [
36 'type' => 'object',
37 'properties' => [
38 'date' => [
39 'type' => 'string',
40 'description' => __('Y-m-d. Defaults to today.', 'fluent-booking'),
41 ],
42 'host_id' => [
43 'type' => 'integer',
44 'description' => __('Brief for one host only.', 'fluent-booking'),
45 ],
46 'timezone' => [
47 'type' => 'string',
48 'description' => __('IANA zone to report times in.', 'fluent-booking'),
49 ],
50 ],
51 ],
52 'is_prompt' => true,
53 'permission_callback' => [PermissionGate::class, 'readGate'],
54 'execute_callback' => [self::class, 'dailyBriefing'],
55 ],
56
57 'fluent-booking/troubleshoot-event' => [
58 'label' => __('Troubleshoot an event type', 'fluent-booking'),
59 'description' => __('Work out why an event type is showing no slots, or the wrong ones, and say what to change.', 'fluent-booking'),
60 'input_schema' => [
61 'type' => 'object',
62 'properties' => [
63 'event_id' => [
64 'type' => 'integer',
65 'description' => __('The event type to investigate.', 'fluent-booking'),
66 ],
67 'complaint' => [
68 'type' => 'string',
69 'description' => __('What the operator or attendee actually reported, in their own words.', 'fluent-booking'),
70 ],
71 'from' => ['type' => 'string'],
72 'to' => ['type' => 'string'],
73 ],
74 'required' => ['event_id'],
75 ],
76 'is_prompt' => true,
77 'permission_callback' => [PermissionGate::class, 'readGate'],
78 'execute_callback' => [self::class, 'troubleshootEvent'],
79 ],
80
81 'fluent-booking/weekly-report' => [
82 'label' => __('Weekly report', 'fluent-booking'),
83 'description' => __('A week\'s booking numbers, with the comparison and the caveats that make them trustworthy.', 'fluent-booking'),
84 'input_schema' => [
85 'type' => 'object',
86 'properties' => [
87 'from' => [
88 'type' => 'string',
89 'description' => __('Y-m-d, start of the period. Defaults to the last 7 days.', 'fluent-booking'),
90 ],
91 'to' => ['type' => 'string'],
92 ],
93 ],
94 'is_prompt' => true,
95 'permission_callback' => [PermissionGate::class, 'readGate'],
96 'execute_callback' => [self::class, 'weeklyReport'],
97 ],
98 ];
99 }
100
101 /**
102 * @param array $params
103 * @return array
104 */
105 public static function dailyBriefing($params = [])
106 {
107 $date = self::date(Arr::get($params, 'date'), gmdate('Y-m-d'));
108 $hostId = absint(Arr::get($params, 'host_id'));
109 $timezone = sanitize_text_field(Arr::get($params, 'timezone', ''));
110
111 $filters = "period: \"all\", from: \"{$date}\", to: \"{$date}\"";
112
113 if ($hostId) {
114 $filters .= ", host_id: {$hostId}";
115 }
116
117 if ($timezone) {
118 $filters .= ", timezone: \"{$timezone}\"";
119 }
120
121 $text = self::lines([
122 "Brief the operator on {$date}.",
123 '',
124 'Do this:',
125 '',
126 '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.',
127 "2. Call `fluent-booking/list-bookings` with {$filters} for the day's schedule.",
128 '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.',
129 '',
130 'Then write the briefing:',
131 '',
132 '- **The day.** Each booking in start order: time (with zone), duration, attendee, event type, host. Say plainly if the day is empty.',
133 '- **Needs a decision.** Pending requests, oldest first, with how long each has been waiting.',
134 '- **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.',
135 '',
136 'Rules:',
137 '',
138 '- If `meta.scope` is `own_calendars`, say so in one line at the top. The operator is seeing their own calendars, not the site.',
139 '- If `meta.pii_masked` is true, do not guess at the masked addresses.',
140 '- 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.',
141 ]);
142
143 return self::prompt(
144 /* translators: %s: the date being briefed */
145 sprintf(__('Daily briefing for %s', 'fluent-booking'), $date),
146 $text
147 );
148 }
149
150 /**
151 * @param array $params
152 * @return array
153 */
154 public static function troubleshootEvent($params = [])
155 {
156 $eventId = absint(Arr::get($params, 'event_id'));
157 $complaint = sanitize_textarea_field(Arr::get($params, 'complaint', ''));
158 $from = self::date(Arr::get($params, 'from'), gmdate('Y-m-d'));
159 $to = self::date(Arr::get($params, 'to'), gmdate('Y-m-d', strtotime('+13 days')));
160
161 $lines = [
162 "Find out why event type {$eventId} is not offering the slots someone expected, between {$from} and {$to}.",
163 ];
164
165 if ($complaint) {
166 $lines[] = '';
167 $lines[] = 'What was reported:';
168 $lines[] = '';
169 $lines[] = '> ' . $complaint;
170 }
171
172 $lines = array_merge($lines, [
173 '',
174 'Do this, in order:',
175 '',
176 "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.",
177 "2. `fluent-booking/get-event-types` with `event_id: {$eventId}` for the configuration behind whichever checks failed.",
178 '3. `fluent-booking/get-available-slots` for the same window, to see what an attendee would actually be offered.',
179 '',
180 'Reading the diagnosis:',
181 '',
182 '- 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.',
183 '- `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.',
184 '- **`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.',
185 '- 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.',
186 '',
187 'Then answer:',
188 '',
189 '1. **What is wrong** — one sentence a non-technical operator understands.',
190 '2. **Why** — the specific setting, with its current value.',
191 '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.',
192 '',
193 '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.',
194 ]);
195
196 return self::prompt(
197 /* translators: %d: the event type id */
198 sprintf(__('Troubleshoot event type %d', 'fluent-booking'), $eventId),
199 self::lines($lines)
200 );
201 }
202
203 /**
204 * @param array $params
205 * @return array
206 */
207 public static function weeklyReport($params = [])
208 {
209 $to = self::date(Arr::get($params, 'to'), gmdate('Y-m-d'));
210 $from = self::date(Arr::get($params, 'from'), gmdate('Y-m-d', strtotime($to . ' -6 days')));
211
212 $span = (int) floor((strtotime($to) - strtotime($from)) / DAY_IN_SECONDS) + 1;
213 $priorTo = gmdate('Y-m-d', strtotime($from . ' -1 day'));
214 $priorFrom = gmdate('Y-m-d', strtotime($priorTo . ' -' . ($span - 1) . ' days'));
215
216 $text = self::lines([
217 "Report on bookings from {$from} to {$to}.",
218 '',
219 'Do this:',
220 '',
221 "1. `fluent-booking/query-bookings` with `from: \"{$from}\"`, `to: \"{$to}\"`, `group_by: [\"status\"]`, `metrics: [\"count\", \"total_minutes\"]` — the headline numbers.",
222 "2. The same call for {$priorFrom} to {$priorTo}, the equal-length period before it, so the comparison is like for like.",
223 '3. `group_by: ["event"]` with `metrics: ["count", "cancellation_rate", "no_show_rate"]` — which event types are working.',
224 '4. `group_by: ["host"]` with `metrics: ["count", "total_minutes"]` — how the load is distributed.',
225 '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`.',
226 '',
227 'Then write the report:',
228 '',
229 '- **Headline.** Total bookings and hours, with the change against the prior period as both a number and a percentage.',
230 '- **By event type.** Ranked. Call out any cancellation or no-show rate that stands apart from the others.',
231 '- **By host.** Ranked by hours, not by count — a host doing four 90-minute sessions is busier than one doing six 15-minute calls.',
232 '- **Patterns.** The busiest weekday and hour, and anything that moved.',
233 '',
234 'Caveats you must carry through, because they change what the numbers mean:',
235 '',
236 '- Report `meta.scope`. On `own_calendars` these are the operator\'s own bookings, not the site\'s.',
237 '- If `meta.truncated` is set, more groups matched than were returned. Say so rather than presenting a partial ranking as complete.',
238 '- `distinct_attendees` cannot be summed across groups without double-counting; the response says so too.',
239 '- 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.',
240 '- A percentage change off a small base is noise. Below about ten bookings, give the raw numbers and skip the percentage.',
241 ]);
242
243 return self::prompt(
244 /* translators: %1$s: start date, %2$s: end date */
245 sprintf(__('Booking report, %1$s to %2$s', 'fluent-booking'), $from, $to),
246 $text
247 );
248 }
249
250 /**
251 * The MCP prompt result shape: a description plus one or more messages.
252 *
253 * @param string $description
254 * @param string $text
255 *
256 * @return array
257 */
258 private static function prompt($description, $text)
259 {
260 return [
261 'description' => $description,
262 'messages' => [
263 [
264 'role' => 'user',
265 'content' => [
266 'type' => 'text',
267 'text' => $text,
268 ],
269 ],
270 ],
271 ];
272 }
273
274 /**
275 * @param array $lines
276 * @return string
277 */
278 private static function lines($lines)
279 {
280 return implode("\n", $lines);
281 }
282
283 /**
284 * @param mixed $value
285 * @param string $fallback
286 *
287 * @return string
288 */
289 private static function date($value, $fallback)
290 {
291 $value = sanitize_text_field((string) $value);
292
293 return MCPHelper::isRealDate($value) ? $value : $fallback;
294 }
295 }
296