Skip to content

Commit c85698c

Browse files
author
Roman Snapko
committed
Refactor chat name generation to handle malformed JSON with repair logic and log errors
1 parent 692c33f commit c85698c

2 files changed

Lines changed: 45 additions & 12 deletions

File tree

packages/server/api/src/app/ai/chat/ai-chat.service.ts

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
AppSystemProp,
99
cacheWrapper,
1010
hashUtils,
11+
logger,
1112
system,
1213
} from '@openops/server-shared';
1314
import {
@@ -23,6 +24,7 @@ import { generateObject, LanguageModel, ModelMessage, UIMessage } from 'ai';
2324
import { z } from 'zod';
2425
import { appConnectionService } from '../../app-connection/app-connection-service/app-connection-service';
2526
import { aiConfigService } from '../config/ai-config.service';
27+
import { findFirstKeyInObject } from '../mcp/llm-query-router';
2628
import { loadPrompt } from './prompts.service';
2729
import { Conversation } from './types';
2830
import {
@@ -100,6 +102,32 @@ const generatedChatNameSchema = z.object({
100102
isGenerated: z.boolean().describe('Whether the name was generated or not'),
101103
});
102104

105+
/**
106+
* Attempts to repair a malformed JSON string produced by the model for chat name generation.
107+
* It extracts only the expected fields according to generatedChatNameSchema.
108+
* Returns null if the input cannot be parsed or repaired (so the AI SDK can retry/throw).
109+
*/
110+
const repairText = (text: string): string | null => {
111+
try {
112+
const parsed = JSON.parse(text);
113+
114+
const nameRaw = findFirstKeyInObject(parsed, 'name');
115+
let name: string | null = null;
116+
if (typeof nameRaw === 'string') {
117+
const trimmed = nameRaw.trim();
118+
name = trimmed.length > 0 ? trimmed.slice(0, 100) : null;
119+
}
120+
121+
const isGeneratedRaw = findFirstKeyInObject(parsed, 'isGenerated');
122+
const isGenerated =
123+
typeof isGeneratedRaw === 'boolean' ? isGeneratedRaw : Boolean(name);
124+
125+
return JSON.stringify({ name, isGenerated });
126+
} catch {
127+
return null;
128+
}
129+
};
130+
103131
export async function generateChatName(
104132
messages: ModelMessage[],
105133
projectId: string,
@@ -117,17 +145,22 @@ export async function generateChatName(
117145
return { name: null, isGenerated: false };
118146
}
119147

120-
const result = await generateObject({
121-
model: languageModel,
122-
system: systemPrompt,
123-
messages: sanitizedMessages,
124-
schema: generatedChatNameSchema,
125-
...aiConfig.modelSettings,
126-
experimental_telemetry: { isEnabled: isLLMTelemetryEnabled() },
127-
maxRetries: 2,
128-
});
129-
130-
return result.object;
148+
try {
149+
const result = await generateObject({
150+
model: languageModel,
151+
system: systemPrompt,
152+
messages: sanitizedMessages,
153+
schema: generatedChatNameSchema,
154+
...aiConfig.modelSettings,
155+
experimental_telemetry: { isEnabled: isLLMTelemetryEnabled() },
156+
experimental_repairText: async ({ text }) => repairText(text),
157+
maxRetries: 2,
158+
});
159+
return result.object;
160+
} catch (error) {
161+
logger.error('Failed to generate chat name', { error });
162+
return { name: null, isGenerated: false };
163+
}
131164
}
132165

133166
export const updateChatName = async (

packages/server/api/src/app/ai/mcp/llm-query-router.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ const getSystemPrompt = async (
222222
* @param targetKey - The key to search for.
223223
* @returns The value of the first key in the object.
224224
*/
225-
function findFirstKeyInObject(
225+
export function findFirstKeyInObject(
226226
obj: Record<string, unknown>,
227227
targetKey: string,
228228
): unknown {

0 commit comments

Comments
 (0)