feat: let get_trip resolve a date code to its trip and date
This commit is contained in:
1 parent
754360605c
commit
3af41c06ab
4 files changed
+45
-9
No files matched your search
+1
-1
@@ -35,7 +35,7 @@ that prices and availability are a snapshot (`dataAsOf`) and only the booking pa
|
||||
| Tool | Use | Returns |
|
||||
|---|---|---|
|
||||
| `search_trips` | Which trips exist, when, where, how much | Up to 10 trips with their next 3 matching dates, `priceFrom`, availability, `dataAsOf` |
|
||||
| `get_trip` | One trip by `code` (optionally `hotelCode`) | Product text with FAQs, hotels with facts, all upcoming dates with bookable rooms, pickups for the first 3 dates |
|
||||
| `get_trip` | One trip by product `code`, or one date of it by date code such as `SSLMP061226` (optionally `hotelCode`) | Product text with FAQs, hotels with facts, all upcoming dates with bookable rooms, pickups for the first 3 dates |
|
||||
| `search_content` | Questions not about dates or prices | Up to 8 hits with a ~600-character excerpt |
|
||||
| `get_document` | The full text of one `search_content` hit, by `url` | Markdown body, truncated at 20 000 characters |
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ use Mcp\Schema\ToolAnnotations;
|
||||
|
||||
#[McpTool(
|
||||
name: 'get_trip',
|
||||
description: 'Full details of one trip: description, included services, programme, hotel, all upcoming dates with rooms, prices per person in EUR and availability, pickup points and departure times, FAQs. Use after search_trips, or when the user names a specific trip. Prices and availability are a snapshot as of dataAsOf; the booking page is binding.',
|
||||
description: 'Full details of one trip: description, included services, programme, hotel, all upcoming dates with rooms, prices per person in EUR and availability, pickup points and departure times, FAQs. Use after search_trips, or when the user names a specific trip or a booked date (date codes such as SSLMP061226 narrow the answer to that date). Prices and availability are a snapshot as of dataAsOf; the booking page is binding.',
|
||||
annotations: new ToolAnnotations(readOnlyHint: true, openWorldHint: false),
|
||||
)]
|
||||
final class GetTripTool
|
||||
@@ -26,7 +26,7 @@ final class GetTripTool
|
||||
}
|
||||
|
||||
public function __invoke(
|
||||
#[Schema(description: 'Trip code, as returned by search_trips.', minLength: 1)]
|
||||
#[Schema(description: 'Trip code as returned by search_trips, or a date code (dateCode) for one date of a trip.', minLength: 1)]
|
||||
string $code,
|
||||
#[Schema(description: 'Narrows the dates to one hotel when a trip has several.')]
|
||||
?string $hotelCode = null,
|
||||
@@ -35,7 +35,7 @@ final class GetTripTool
|
||||
$trip = $this->catalog->get(trim($code), $hotelCode);
|
||||
|
||||
if (null === $trip) {
|
||||
throw new ToolCallException(sprintf('No trip with code "%s". Use search_trips to find trip codes.', $code));
|
||||
throw new ToolCallException(sprintf('No trip or date with code "%s". Use search_trips to find trip codes.', $code));
|
||||
}
|
||||
|
||||
return $trip;
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Chatbot;
|
||||
|
||||
use App\BusProNet\Utility\DateCodeUtility;
|
||||
use App\Chatbot\Search\ContentSearchQuery;
|
||||
use App\Entity\ContentDocument;
|
||||
use App\Model\Chatbot\TripCriteria;
|
||||
@@ -79,6 +80,7 @@ class TripCatalog
|
||||
$current = $dates[$dateId] ?? null;
|
||||
|
||||
$dates[$dateId] = [
|
||||
'dateCode' => $row['date']['code'],
|
||||
'dateFrom' => $row['date']['from'],
|
||||
'dateTo' => $row['date']['to'],
|
||||
'priceFrom' => null === $current ? $summary['priceFrom'] : $this->minPrice($current['priceFrom'], $summary['priceFrom']),
|
||||
@@ -107,12 +109,29 @@ class TripCatalog
|
||||
/**
|
||||
* Everything about one trip, or null for a code no published product carries.
|
||||
*
|
||||
* Takes a product code or a date code. A date code is what customers read on their
|
||||
* booking documents (SSLMP061226 is product SSLMP on 06.12.26); it narrows the trip to
|
||||
* that date, shown even when it has already started.
|
||||
*
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function get(string $code, ?string $hotelCode = null): ?array
|
||||
{
|
||||
$catalog = $this->load();
|
||||
$products = $catalog['productsByCode'][$this->normalize($code)] ?? [];
|
||||
$code = (new DateCodeUtility())->sanitize(trim($code));
|
||||
$dateCode = null;
|
||||
|
||||
if (!isset($catalog['productsByCode'][$code])) {
|
||||
foreach ($catalog['export']->rows as $row) {
|
||||
if ($this->normalize($row['date']['code'] ?? null) === $code) {
|
||||
$dateCode = $code;
|
||||
$code = $this->normalize($row['product']['code'] ?? null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$products = $catalog['productsByCode'][$code] ?? [];
|
||||
|
||||
if ([] === $products) {
|
||||
return null;
|
||||
@@ -123,8 +142,10 @@ class TripCatalog
|
||||
$today = $this->today();
|
||||
|
||||
$rows = array_values(array_filter(
|
||||
$catalog['rowsByCode'][$this->normalize($code)] ?? [],
|
||||
fn (array $row): bool => $row['date']['from'] >= $today,
|
||||
$catalog['rowsByCode'][$code] ?? [],
|
||||
fn (array $row): bool => null === $dateCode
|
||||
? $row['date']['from'] >= $today
|
||||
: $this->normalize($row['date']['code'] ?? null) === $dateCode,
|
||||
));
|
||||
|
||||
if (null !== $hotelCode && '' !== trim($hotelCode)) {
|
||||
@@ -545,6 +566,7 @@ class TripCatalog
|
||||
}
|
||||
|
||||
$dates[$dateId] ??= [
|
||||
'dateCode' => $row['date']['code'],
|
||||
'dateFrom' => $row['date']['from'],
|
||||
'dateTo' => $row['date']['to'],
|
||||
'hotels' => [],
|
||||
|
||||
@@ -33,8 +33,8 @@ class TripCatalogTest extends TestCase
|
||||
self::assertSame(['country' => 'Frankreich'], $trip['destination']);
|
||||
self::assertSame(['Hotel Eins'], $trip['hotels']);
|
||||
self::assertSame([
|
||||
['dateFrom' => '2026-12-05', 'dateTo' => '2026-12-12', 'priceFrom' => 450.0, 'availability' => 'frei'],
|
||||
['dateFrom' => '2027-01-09', 'dateTo' => '2027-01-16', 'priceFrom' => 520.0, 'availability' => 'auf Anfrage'],
|
||||
['dateCode' => 'VTH2', 'dateFrom' => '2026-12-05', 'dateTo' => '2026-12-12', 'priceFrom' => 450.0, 'availability' => 'frei'],
|
||||
['dateCode' => 'VTH3', 'dateFrom' => '2027-01-09', 'dateTo' => '2027-01-16', 'priceFrom' => 520.0, 'availability' => 'auf Anfrage'],
|
||||
], $trip['nextDates']);
|
||||
self::assertSame('2026-10-08T06:00:00+00:00', $result['dataAsOf']);
|
||||
}
|
||||
@@ -93,6 +93,20 @@ class TripCatalogTest extends TestCase
|
||||
self::assertSame('GRP', $this->catalog($rows)->get('GRP')['code'] ?? null);
|
||||
}
|
||||
|
||||
public function testGetTakesADateCodeAndNarrowsToThatDate(): void
|
||||
{
|
||||
$catalog = $this->catalog([
|
||||
$this->row('VTH', 2, '2026-12-05', 'HOT1', [$this->room(450)]),
|
||||
$this->row('VTH', 3, '2027-01-09', 'HOT1', [$this->room(520)]),
|
||||
]);
|
||||
|
||||
$trip = $catalog->get('vth-3');
|
||||
|
||||
self::assertSame('VTH', $trip['code'] ?? null);
|
||||
self::assertSame(['VTH3'], array_column($trip['dates'], 'dateCode'));
|
||||
self::assertNull($catalog->get('VTH9'));
|
||||
}
|
||||
|
||||
public function testGetIsNullForACodeWithoutAPublishedProduct(): void
|
||||
{
|
||||
self::assertNull($this->catalog([$this->row('XYZ', 2, '2026-12-05', 'HOT1', [$this->room(1)])])->get('XYZ'));
|
||||
|
||||
Reference in new issue
Block a user