mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 14:36:31 +00:00
fix(booking-import): say why a recognised booking could not be imported
A document the model read correctly could still come back as "No reservations could be extracted". Every type mapper can answer null when the node carries no reservationFor, or one with no name, and both call sites pushed the item only if there was one — with no else. The unknown-@type branch next to them does warn, so a node TREK did not recognise was reported while a node it recognised but could not map vanished: no item, no warning, nothing in the log. Because the file did produce a node, the "no reservations found" fallback was skipped too, and the preview arrived empty and silent. Every recognised type could disappear this way, on both the kitinerary and the AI path. Each of them now warns, naming the file, the index, the type and the reason. Three smaller silences went with it. The debug line labelled "Raw LLM Response" printed the already-parsed array, so its inspect formatting — single quotes, bare keys — read as a provider emitting broken JSON and sent this report at the JSON parser, which has handled that shape since 4.0.0. A single reservation answered on its own, with no array and no wrapper, was dropped by the list unwrapper although the schema downstream would have taken it. And a response nobody could read returned an empty list, which is the same answer as a voucher with no booking in it; it now raises, on the Anthropic client too, where a reply cut off at the token limit produced exactly this report's symptom. The plugin AI surface keeps its old empty answer, since a value is what a plugin contracts for. The retry ladder gained a rung that sends no response_format at all, for providers that support neither json_schema nor json_object. Closes #2375
This commit is contained in:
@@ -9,6 +9,9 @@ import { RealtimeService } from '../../../realtime/realtime.service';
|
||||
import { NotificationsService } from '../../../notifications/notifications.service';
|
||||
import { LlmConfigResolver } from '../../../llm-parse/llm-config.resolver';
|
||||
import { createLlmClient } from '../../../llm-parse/llm-client.factory';
|
||||
import { UnreadableLlmResponse } from '../../../llm-parse/clients/openai-compatible.client';
|
||||
import type { ResolvedLlmConfig } from '../../../llm-parse/llm-config';
|
||||
import type { LlmExtractionInput } from '../../../llm-parse/llm-provider.interface';
|
||||
import { PluginOAuthService } from '../../oauth/plugin-oauth.service';
|
||||
import { stripEmoji } from '../../text-sanitize';
|
||||
|
||||
@@ -125,7 +128,7 @@ export class HostSurfaceRpc {
|
||||
if (prompt.length > AI_TEXT_MAX) throw new BadParams(`prompt exceeds the ${AI_TEXT_MAX}-char cap`);
|
||||
this.takeAiBudget(ctx);
|
||||
const system = typeof params.system === 'string' ? params.system.slice(0, 4000) : undefined;
|
||||
const results = await createLlmClient(config).extract({
|
||||
const results = await this.runModel(config, {
|
||||
prompt: system || 'You are a helpful assistant. Reply with a JSON object of the form {"text": "..."} whose "text" field holds your answer.',
|
||||
jsonSchema: { type: 'object', properties: { text: { type: 'string' } }, required: ['text'] },
|
||||
model: config.model,
|
||||
@@ -149,7 +152,7 @@ export class HostSurfaceRpc {
|
||||
}
|
||||
this.takeAiBudget(ctx);
|
||||
const hint = typeof params.prompt === 'string' ? params.prompt.slice(0, 4000) : '';
|
||||
const results = await createLlmClient(config).extract({
|
||||
const results = await this.runModel(config, {
|
||||
prompt: hint || 'Extract structured data from the text into the given JSON schema.',
|
||||
jsonSchema: params.jsonSchema as object,
|
||||
model: config.model,
|
||||
@@ -224,6 +227,24 @@ export class HostSurfaceRpc {
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* The model's answer, without the diagnosis the booking import asked for.
|
||||
*
|
||||
* A client raises `UnreadableLlmResponse` when the model replies with prose or
|
||||
* with nothing at all, so an import can warn about that file (#2375). Plugins
|
||||
* were written against the older contract here — an empty answer, never an
|
||||
* error — and this surface is versioned on its own, so that one stays empty. A
|
||||
* request that actually failed still reaches the plugin as HOST_ERROR.
|
||||
*/
|
||||
private async runModel(config: ResolvedLlmConfig, input: LlmExtractionInput): Promise<Record<string, unknown>[]> {
|
||||
try {
|
||||
return await createLlmClient(config).extract(input);
|
||||
} catch (err) {
|
||||
if (err instanceof UnreadableLlmResponse) return [];
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private takeAiBudget(ctx: PluginRpcContext): void {
|
||||
if (!budgetFor(ctx.pluginId, this.db.connection).take('ai', Date.now())) {
|
||||
throw new BadParams('daily AI budget exhausted (resets at UTC midnight)');
|
||||
|
||||
Reference in New Issue
Block a user