@contentgrowth/llm-service 0.7.0 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentgrowth/llm-service",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "Unified LLM Service for Content Growth",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -90,6 +90,26 @@ export class GeminiProvider extends BaseLLMProvider {
90
90
  case 'user':
91
91
  role = 'user';
92
92
  parts = [{ text: msg.content }];
93
+
94
+ // Enhancement: If this is the LAST message (current turn), append the reminder.
95
+ // This helps the model respect the system prompt (especially format) even with long context history.
96
+ if (index === geminiMessages.length - 1) {
97
+ let reminder = "";
98
+ if (options.responseFormat === 'json' || options.responseFormat?.type === 'json_schema' || options.responseSchema) {
99
+ reminder = "\n\n[SYSTEM NOTE: The output MUST be valid JSON as per the schema. Do not include markdown formatting or explanations.]";
100
+ } else {
101
+ reminder = "\n\n[SYSTEM NOTE: Please ensure your response adheres strictly to the constraints defined in the System Prompt.]";
102
+ }
103
+
104
+ // Append to the existing text part (Safest method)
105
+ const lastPart = parts.find(p => p.text);
106
+ if (lastPart) {
107
+ lastPart.text += reminder;
108
+ } else {
109
+ // Fallback if message was image-only
110
+ parts.push({ text: reminder });
111
+ }
112
+ }
93
113
  break;
94
114
  case 'assistant':
95
115
  role = 'model';
@@ -115,11 +135,11 @@ export class GeminiProvider extends BaseLLMProvider {
115
135
  // Fix for JSON mode: If JSON is requested, remind the model to output JSON after tool execution
116
136
  // This is necessary because strict JSON mode is disabled when tools are present.
117
137
  if (options.responseFormat === 'json' || options.responseFormat?.type === 'json_schema' || options.responseSchema) {
118
- parts.push({ text: "Please ensure the final response is valid JSON as per the system instructions." });
138
+ parts.push({ text: "\n\n[SYSTEM NOTE: The output MUST be valid JSON as per the schema. Do not include markdown formatting or explanations.]" });
119
139
  } else {
120
140
  // Generic reminder to help model stay on track with system prompt instructions (e.g. formatting)
121
141
  // even if no specific JSON mode is configured.
122
- parts.push({ text: "Please ensure the final response follows the system prompt instructions." });
142
+ parts.push({ text: "\n\n[SYSTEM NOTE: Please ensure your response adheres strictly to the constraints defined in the System Prompt.]" });
123
143
  }
124
144
  break;
125
145
  default:
@@ -160,7 +180,7 @@ export class GeminiProvider extends BaseLLMProvider {
160
180
  }
161
181
  }
162
182
 
163
- console.log('[GeminiProvider] generateContent request:', JSON.stringify(requestOptions, null, 2));
183
+ // console.log('[GeminiProvider] generateContent request:', JSON.stringify(requestOptions, null, 2));
164
184
 
165
185
  let response;
166
186
  try {
@@ -208,7 +228,7 @@ export class GeminiProvider extends BaseLLMProvider {
208
228
  );
209
229
  }
210
230
 
211
- console.log('Gemini returns:', textContent);
231
+ // console.log('Gemini returns:', textContent);
212
232
  // Return with parsed JSON if applicable
213
233
  return {
214
234
  content: textContent,
@@ -245,7 +265,7 @@ export class GeminiProvider extends BaseLLMProvider {
245
265
  // Use responseSchema for strict structured output
246
266
  // Must convert to Gemini Schema format (Uppercase types)
247
267
  config.responseSchema = this._convertToGeminiSchema(schema);
248
- console.log('[GeminiProvider] Using Strict JSON mode with schema (responseSchema)');
268
+ // console.log('[GeminiProvider] Using Strict JSON mode with schema (responseSchema)');
249
269
  } else {
250
270
  console.warn('[GeminiProvider] Using legacy JSON mode without schema - may produce markdown wrappers');
251
271
  }
@@ -316,9 +336,7 @@ export class GeminiProvider extends BaseLLMProvider {
316
336
  // - Brace extraction as fallback
317
337
  const parsed = extractJsonFromResponse(content);
318
338
 
319
- if (parsed) {
320
- console.log('[GeminiProvider] Successfully parsed JSON from response');
321
- } else {
339
+ if (!parsed) {
322
340
  console.error('[GeminiProvider] Failed to extract valid JSON from response');
323
341
  console.error('[GeminiProvider] Content preview:', content.substring(0, 200));
324
342
  }
@@ -334,7 +352,7 @@ export class GeminiProvider extends BaseLLMProvider {
334
352
  const tool_call_id = `gemini-tool-call-${index}`;
335
353
  toolCall.id = tool_call_id;
336
354
 
337
- console.log(`[Tool Call] ${toolName} with arguments:`, toolCall.function.args);
355
+ // console.log(`[Tool Call] ${toolName} with arguments:`, toolCall.function.args);
338
356
 
339
357
  if (!tool) {
340
358
  console.error(`[Tool Error] Tool '${toolName}' not found`);
@@ -342,7 +360,7 @@ export class GeminiProvider extends BaseLLMProvider {
342
360
  }
343
361
  try {
344
362
  const output = await tool(toolCall.function.args, { env, tenantId });
345
- console.log(`[Tool Result] ${toolName} returned:`, output.substring(0, 200) + (output.length > 200 ? '...' : ''));
363
+ // console.log(`[Tool Result] ${toolName} returned:`, output.substring(0, 200) + (output.length > 200 ? '...' : ''));
346
364
  return { tool_call_id, output };
347
365
  } catch (error) {
348
366
  console.error(`[Tool Error] ${toolName} failed:`, error.message);
@@ -391,7 +409,7 @@ export class GeminiProvider extends BaseLLMProvider {
391
409
  requestOptions.config.systemInstruction = { parts: [{ text: systemPrompt }] };
392
410
  }
393
411
 
394
- console.log('[GeminiProvider] imageGeneration request:', JSON.stringify(requestOptions, null, 2));
412
+ // console.log('[GeminiProvider] imageGeneration request:', JSON.stringify(requestOptions, null, 2));
395
413
 
396
414
  const response = await this.client.models.generateContent(requestOptions);
397
415
 
@@ -429,9 +447,12 @@ export class GeminiProvider extends BaseLLMProvider {
429
447
 
430
448
  async startVideoGeneration(prompt, images, modelName, systemPrompt, options = {}) {
431
449
  // Use unified client for video generation
432
- const operation = await this.client.models.generateVideos({
450
+ // Prepend system prompt to user prompt if provided, as video models often expect instructions in the prompt
451
+ const effectivePrompt = systemPrompt ? `${systemPrompt}\n\n${prompt}` : prompt;
452
+
453
+ const requestConfig = {
433
454
  model: modelName,
434
- prompt: prompt,
455
+ prompt: effectivePrompt,
435
456
  config: {
436
457
  durationSeconds: options.durationSeconds || 6,
437
458
  aspectRatio: options.aspectRatio || '16:9',
@@ -439,15 +460,35 @@ export class GeminiProvider extends BaseLLMProvider {
439
460
  // Pass reference images if provided
440
461
  ...(images && images.length > 0 ? { referenceImages: images } : {}),
441
462
  }
442
- });
463
+ };
464
+
465
+ // Create a loggable copy of the config
466
+ const logConfig = JSON.parse(JSON.stringify(requestConfig));
467
+ if (logConfig.config && logConfig.config.referenceImages) {
468
+ logConfig.config.referenceImages = logConfig.config.referenceImages.map(img => ({
469
+ ...img,
470
+ data: `... (${img.data ? img.data.length : 0} bytes)` // Summarize data
471
+ }));
472
+ }
473
+
474
+ console.log('[GeminiProvider] startVideoGeneration request:', JSON.stringify(logConfig, null, 2));
443
475
 
444
- // Store operation for later polling
445
- this._pendingOperations.set(operation.name, operation);
476
+ try {
477
+ const operation = await this.client.models.generateVideos(requestConfig);
446
478
 
447
- return { operationName: operation.name };
479
+ // Store operation for later polling
480
+ this._pendingOperations.set(operation.name, operation);
481
+
482
+ return { operationName: operation.name };
483
+ } catch (error) {
484
+ console.error('[GeminiProvider] startVideoGeneration failed:', error);
485
+ throw error;
486
+ }
448
487
  }
449
488
 
450
489
  async getVideoGenerationStatus(operationName) {
490
+ console.log(`[GeminiProvider] Checking status for operation: ${operationName}`);
491
+
451
492
  // Get the operation from cache or fetch it
452
493
  let operation = this._pendingOperations.get(operationName);
453
494
 
@@ -468,6 +509,8 @@ export class GeminiProvider extends BaseLLMProvider {
468
509
  state: operation.metadata?.state || (operation.done ? 'COMPLETED' : 'PROCESSING'),
469
510
  };
470
511
 
512
+ console.log(`[GeminiProvider] Operation status: ${result.state}, Progress: ${result.progress}%`);
513
+
471
514
  if (operation.done) {
472
515
  // Clean up from cache
473
516
  this._pendingOperations.delete(operationName);
@@ -159,18 +159,19 @@ export class LLMService {
159
159
  }
160
160
  }
161
161
 
162
- async chatWithTools(messages, tenantId, systemPrompt, tools = []) {
162
+ async chatWithTools(messages, tenantId, systemPrompt, tools = [], options = {}) {
163
163
  const provider = await this._getProvider(tenantId);
164
164
 
165
165
  let currentMessages = [...messages];
166
166
  const MAX_ITERATIONS = 10; // Prevent infinite loops
167
167
  let iteration = 0;
168
168
 
169
- // Initial call - no options since tools + JSON mode are incompatible
169
+ // Initial call
170
170
  const initialResponse = await provider.chatCompletion(
171
171
  currentMessages,
172
172
  systemPrompt,
173
- tools
173
+ tools,
174
+ options
174
175
  );
175
176
 
176
177
  let { content, tool_calls, parsedContent } = initialResponse;
@@ -184,11 +185,12 @@ export class LLMService {
184
185
  // Execute tools using the provider's helper (which formats results for that provider)
185
186
  await provider.executeTools(tool_calls, currentMessages, tenantId, this.toolImplementations, this.env);
186
187
 
187
- // Next call - no options
188
+ // Next call
188
189
  const nextResponse = await provider.chatCompletion(
189
190
  currentMessages,
190
191
  systemPrompt,
191
- tools
192
+ tools,
193
+ options
192
194
  );
193
195
 
194
196
  content = nextResponse.content;