@smythos/sre 1.7.41 → 1.8.0
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/CHANGELOG +136 -64
- package/dist/index.js +65 -50
- package/dist/index.js.map +1 -1
- package/dist/types/Components/Async.class.d.ts +11 -5
- package/dist/types/index.d.ts +2 -0
- package/dist/types/subsystems/AgentManager/AgentData.service/connectors/SQLiteAgentDataConnector.class.d.ts +45 -0
- package/dist/types/subsystems/LLMManager/LLM.helper.d.ts +32 -1
- package/dist/types/subsystems/LLMManager/LLM.inference.d.ts +25 -2
- package/dist/types/subsystems/LLMManager/LLM.service/connectors/Anthropic.class.d.ts +22 -2
- package/dist/types/subsystems/LLMManager/LLM.service/connectors/Bedrock.class.d.ts +2 -2
- package/dist/types/subsystems/LLMManager/LLM.service/connectors/GoogleAI.class.d.ts +27 -2
- package/dist/types/subsystems/LLMManager/LLM.service/connectors/Groq.class.d.ts +22 -2
- package/dist/types/subsystems/LLMManager/LLM.service/connectors/Ollama.class.d.ts +22 -2
- package/dist/types/subsystems/LLMManager/LLM.service/connectors/Perplexity.class.d.ts +3 -3
- package/dist/types/subsystems/LLMManager/LLM.service/connectors/openai/OpenAIConnector.class.d.ts +23 -3
- package/dist/types/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/ChatCompletionsApiInterface.d.ts +2 -2
- package/dist/types/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/OpenAIApiInterface.d.ts +2 -2
- package/dist/types/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/ResponsesApiInterface.d.ts +2 -2
- package/dist/types/subsystems/LLMManager/LLM.service/connectors/xAI.class.d.ts +3 -3
- package/dist/types/subsystems/MemoryManager/LLMContext.d.ts +10 -3
- package/dist/types/subsystems/ObservabilityManager/Telemetry.service/connectors/OTel/OTel.class.d.ts +24 -0
- package/dist/types/subsystems/ObservabilityManager/Telemetry.service/connectors/OTel/OTel.redaction.helper.d.ts +49 -0
- package/dist/types/types/LLM.types.d.ts +30 -1
- package/package.json +4 -3
- package/src/Components/APICall/OAuth.helper.ts +16 -1
- package/src/Components/APIEndpoint.class.ts +11 -4
- package/src/Components/Async.class.ts +38 -5
- package/src/Components/GenAILLM.class.ts +13 -7
- package/src/Components/LLMAssistant.class.ts +3 -1
- package/src/Components/LogicAND.class.ts +13 -0
- package/src/Components/LogicAtLeast.class.ts +18 -0
- package/src/Components/LogicAtMost.class.ts +19 -0
- package/src/Components/LogicOR.class.ts +12 -2
- package/src/Components/LogicXOR.class.ts +11 -0
- package/src/constants.ts +1 -1
- package/src/helpers/Conversation.helper.ts +10 -8
- package/src/index.ts +2 -0
- package/src/index.ts.bak +2 -0
- package/src/subsystems/AgentManager/AgentData.service/connectors/SQLiteAgentDataConnector.class.ts +190 -0
- package/src/subsystems/AgentManager/AgentData.service/index.ts +2 -0
- package/src/subsystems/LLMManager/LLM.helper.ts +117 -1
- package/src/subsystems/LLMManager/LLM.inference.ts +136 -67
- package/src/subsystems/LLMManager/LLM.service/LLMConnector.ts +13 -6
- package/src/subsystems/LLMManager/LLM.service/connectors/Anthropic.class.ts +157 -33
- package/src/subsystems/LLMManager/LLM.service/connectors/Bedrock.class.ts +9 -8
- package/src/subsystems/LLMManager/LLM.service/connectors/GoogleAI.class.ts +121 -83
- package/src/subsystems/LLMManager/LLM.service/connectors/Groq.class.ts +125 -62
- package/src/subsystems/LLMManager/LLM.service/connectors/Ollama.class.ts +168 -76
- package/src/subsystems/LLMManager/LLM.service/connectors/Perplexity.class.ts +18 -8
- package/src/subsystems/LLMManager/LLM.service/connectors/VertexAI.class.ts +8 -4
- package/src/subsystems/LLMManager/LLM.service/connectors/openai/OpenAIConnector.class.ts +50 -8
- package/src/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/ChatCompletionsApiInterface.ts +30 -16
- package/src/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/OpenAIApiInterface.ts +2 -2
- package/src/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/ResponsesApiInterface.ts +29 -15
- package/src/subsystems/LLMManager/LLM.service/connectors/xAI.class.ts +10 -8
- package/src/subsystems/MemoryManager/LLMContext.ts +27 -8
- package/src/subsystems/ObservabilityManager/Telemetry.service/connectors/OTel/OTel.class.ts +467 -120
- package/src/subsystems/ObservabilityManager/Telemetry.service/connectors/OTel/OTel.redaction.helper.ts +203 -0
- package/src/types/LLM.types.ts +31 -1
- package/src/types/node-sqlite.d.ts +45 -0
package/src/subsystems/AgentManager/AgentData.service/connectors/SQLiteAgentDataConnector.class.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
4
|
+
import { AgentDataConnector } from '../AgentDataConnector';
|
|
5
|
+
import { JSONContentHelper } from '@sre/helpers/JsonContent.helper';
|
|
6
|
+
import { Logger } from '@sre/helpers/Log.helper';
|
|
7
|
+
|
|
8
|
+
const console = Logger('SQLiteAgentDataConnector');
|
|
9
|
+
|
|
10
|
+
export type SQLiteAgentDataSettings = {
|
|
11
|
+
databasePath: string;
|
|
12
|
+
tableName?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type SQLiteRunResult = { changes?: number; lastInsertRowid?: number | bigint };
|
|
16
|
+
|
|
17
|
+
export interface SQLiteStatementAdapter<T = any> {
|
|
18
|
+
all(...params: any[]): T[];
|
|
19
|
+
get(...params: any[]): T | undefined;
|
|
20
|
+
run(...params: any[]): SQLiteRunResult;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface SQLiteDatabaseAdapter {
|
|
24
|
+
prepare<T = any>(sql: string): SQLiteStatementAdapter<T>;
|
|
25
|
+
exec(sql: string): void;
|
|
26
|
+
close(): void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type SQLiteAdapterFactory = (databasePath: string) => SQLiteDatabaseAdapter;
|
|
30
|
+
|
|
31
|
+
class NativeSQLiteAdapter implements SQLiteDatabaseAdapter {
|
|
32
|
+
private db: DatabaseSync;
|
|
33
|
+
|
|
34
|
+
constructor(databasePath: string) {
|
|
35
|
+
this.db = new DatabaseSync(databasePath);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public prepare<T = any>(sql: string) {
|
|
39
|
+
return this.db.prepare(sql) as unknown as SQLiteStatementAdapter<T>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public exec(sql: string) {
|
|
43
|
+
this.db.exec(sql);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public close() {
|
|
47
|
+
this.db.close();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type AgentRow = {
|
|
52
|
+
id: number;
|
|
53
|
+
data: any;
|
|
54
|
+
aiAgentId: string;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export class SQLiteAgentDataConnector extends AgentDataConnector {
|
|
58
|
+
public name = 'SQLiteAgentDataConnector';
|
|
59
|
+
private adapter!: SQLiteDatabaseAdapter;
|
|
60
|
+
private readonly tableName: string;
|
|
61
|
+
|
|
62
|
+
constructor(protected _settings: SQLiteAgentDataSettings) {
|
|
63
|
+
super(_settings);
|
|
64
|
+
this.tableName = this.validateTableName(_settings?.tableName || 'AiAgentData');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public getAgentConfig(agentId: string): Partial<SQLiteAgentDataSettings> {
|
|
68
|
+
return {
|
|
69
|
+
databasePath: this._settings?.databasePath,
|
|
70
|
+
tableName: this.tableName,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public async start() {
|
|
75
|
+
super.start();
|
|
76
|
+
this.started = false;
|
|
77
|
+
|
|
78
|
+
if (!this._settings?.databasePath) {
|
|
79
|
+
throw new Error('SQLiteAgentDataConnector requires a databasePath setting');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this.ensureDatabaseDirectory(this._settings.databasePath);
|
|
83
|
+
this.adapter = new NativeSQLiteAdapter(this._settings.databasePath);
|
|
84
|
+
this.ensureSchema();
|
|
85
|
+
|
|
86
|
+
this.started = true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public async stop() {
|
|
90
|
+
try {
|
|
91
|
+
this.adapter?.close();
|
|
92
|
+
} catch (error: any) {
|
|
93
|
+
console.warn('Error closing SQLite adapter', error?.message || error);
|
|
94
|
+
}
|
|
95
|
+
await super.stop();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
public async getAgentData(agentId: string, version?: string) {
|
|
99
|
+
const ready = await this.ready();
|
|
100
|
+
if (!ready) {
|
|
101
|
+
throw new Error('Connector not ready');
|
|
102
|
+
}
|
|
103
|
+
const row = this.prepareStatement<AgentRow>(`SELECT id, data, aiAgentId FROM "${this.tableName}" WHERE aiAgentId = ? LIMIT 1`).get(agentId);
|
|
104
|
+
|
|
105
|
+
if (!row) {
|
|
106
|
+
throw new Error(`Agent with id ${agentId} not found`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const parsed = this.parseData(row.data);
|
|
110
|
+
return {
|
|
111
|
+
data: parsed,
|
|
112
|
+
version: version || parsed?.version || '1.0',
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
public async getAgentIdByDomain(domain: string): Promise<string> {
|
|
117
|
+
return Promise.resolve('');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
public async getAgentSettings(agentId: string, version?: string) {
|
|
121
|
+
const ready = await this.ready();
|
|
122
|
+
if (!ready) {
|
|
123
|
+
throw new Error('Connector not ready');
|
|
124
|
+
}
|
|
125
|
+
const agent = await this.getAgentData(agentId, version);
|
|
126
|
+
return agent?.data?.settings || {};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
public async getAgentEmbodiments(agentId: string): Promise<any> {
|
|
130
|
+
const ready = await this.ready();
|
|
131
|
+
if (!ready) {
|
|
132
|
+
throw new Error('Connector not ready');
|
|
133
|
+
}
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
public async listTeamAgents(teamId: string, deployedOnly?: boolean, includeData?: boolean): Promise<any[]> {
|
|
138
|
+
const ready = await this.ready();
|
|
139
|
+
if (!ready) {
|
|
140
|
+
throw new Error('Connector not ready');
|
|
141
|
+
}
|
|
142
|
+
console.warn(`listTeamAgents is not implemented for SQLiteAgentDataConnector`);
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
public async isDeployed(agentId: string): Promise<boolean> {
|
|
147
|
+
const ready = await this.ready();
|
|
148
|
+
if (!ready) {
|
|
149
|
+
throw new Error('Connector not ready');
|
|
150
|
+
}
|
|
151
|
+
const record = await this.getAgentData(agentId).catch(() => null);
|
|
152
|
+
return !!record;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private ensureDatabaseDirectory(databasePath: string) {
|
|
156
|
+
if (databasePath === ':memory:') return;
|
|
157
|
+
const dbDir = path.dirname(path.resolve(databasePath));
|
|
158
|
+
if (!fs.existsSync(dbDir)) {
|
|
159
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private ensureSchema() {
|
|
164
|
+
const ddl = `
|
|
165
|
+
CREATE TABLE IF NOT EXISTS "${this.tableName}" (
|
|
166
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
167
|
+
data TEXT CHECK (json_valid(data)),
|
|
168
|
+
aiAgentId TEXT NOT NULL UNIQUE
|
|
169
|
+
);
|
|
170
|
+
`;
|
|
171
|
+
this.adapter.exec(ddl);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private validateTableName(tableName: string) {
|
|
175
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(tableName)) {
|
|
176
|
+
throw new Error(`Invalid SQLite table name: ${tableName}`);
|
|
177
|
+
}
|
|
178
|
+
return tableName;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private prepareStatement<T = any>(sql: string) {
|
|
182
|
+
return this.adapter.prepare<T>(sql);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private parseData(data: any) {
|
|
186
|
+
if (Buffer.isBuffer(data)) data = data.toString('utf-8');
|
|
187
|
+
if (typeof data === 'string') return JSONContentHelper.create(data).tryParse();
|
|
188
|
+
return data;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -6,12 +6,14 @@ import { CLIAgentDataConnector } from './connectors/CLIAgentDataConnector.class'
|
|
|
6
6
|
import { AgentDataConnector } from './AgentDataConnector';
|
|
7
7
|
import { LocalAgentDataConnector } from './connectors/LocalAgentDataConnector.class';
|
|
8
8
|
import { NullAgentData } from './connectors/NullAgentData.class';
|
|
9
|
+
import { SQLiteAgentDataConnector } from './connectors/SQLiteAgentDataConnector.class';
|
|
9
10
|
export class AgentDataService extends ConnectorServiceProvider {
|
|
10
11
|
public register() {
|
|
11
12
|
//FIXME : register an actual account connector, not the abstract one
|
|
12
13
|
ConnectorService.register(TConnectorService.AgentData, 'AgentData', AgentDataConnector);
|
|
13
14
|
ConnectorService.register(TConnectorService.AgentData, 'CLI', CLIAgentDataConnector);
|
|
14
15
|
ConnectorService.register(TConnectorService.AgentData, 'Local', LocalAgentDataConnector);
|
|
16
|
+
ConnectorService.register(TConnectorService.AgentData, 'SQLite', SQLiteAgentDataConnector);
|
|
15
17
|
|
|
16
18
|
ConnectorService.register(TConnectorService.AgentData, 'NullAgentData', NullAgentData);
|
|
17
19
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type TLLMMessageBlock, TLLMMessageRole } from '@sre/types/LLM.types';
|
|
1
|
+
import { type TLLMMessageBlock, TLLMMessageRole, TLLMFinishReason } from '@sre/types/LLM.types';
|
|
2
2
|
|
|
3
3
|
import axios from 'axios';
|
|
4
4
|
import imageSize from 'image-size';
|
|
@@ -273,4 +273,120 @@ export class LLMHelper {
|
|
|
273
273
|
// Examples: claude-opus-4-5, claude-sonnet-4-20250514, claude-4-opus
|
|
274
274
|
return /claude-(?:\w+-)?4(?:-|$)/i.test(modelId);
|
|
275
275
|
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Normalizes provider-specific finish reason values to TLLMFinishReason enum.
|
|
279
|
+
* Handles provider-specific values from OpenAI, Anthropic, Google AI, and other providers.
|
|
280
|
+
*
|
|
281
|
+
* @param finishReason - The finish reason from the provider (can be string, null, or undefined)
|
|
282
|
+
* @returns Normalized TLLMFinishReason enum value
|
|
283
|
+
*
|
|
284
|
+
* @example
|
|
285
|
+
* const normalized = LLMHelper.normalizeFinishReason('end_turn');
|
|
286
|
+
* console.log(normalized); // TLLMFinishReason.Stop
|
|
287
|
+
*
|
|
288
|
+
* @example
|
|
289
|
+
* const normalized = LLMHelper.normalizeFinishReason('tool_use');
|
|
290
|
+
* console.log(normalized); // TLLMFinishReason.ToolCalls
|
|
291
|
+
*
|
|
292
|
+
* @example
|
|
293
|
+
* const normalized = LLMHelper.normalizeFinishReason('SAFETY');
|
|
294
|
+
* console.log(normalized); // TLLMFinishReason.ContentFilter
|
|
295
|
+
*/
|
|
296
|
+
public static normalizeFinishReason(finishReason: string | null | undefined): TLLMFinishReason {
|
|
297
|
+
if (!finishReason) {
|
|
298
|
+
return TLLMFinishReason.Stop;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const normalized = finishReason.toLowerCase().trim();
|
|
302
|
+
|
|
303
|
+
// Map standard and provider-specific values
|
|
304
|
+
switch (normalized) {
|
|
305
|
+
// Natural stop
|
|
306
|
+
case 'stop':
|
|
307
|
+
case 'end_turn': // Anthropic - natural end of turn
|
|
308
|
+
case 'stop_sequence': // Anthropic - custom stop sequence matched
|
|
309
|
+
case 'pause_turn': // Anthropic - paused for long-running operation
|
|
310
|
+
return TLLMFinishReason.Stop;
|
|
311
|
+
|
|
312
|
+
// Token/length limits
|
|
313
|
+
case 'length':
|
|
314
|
+
case 'max_tokens': // Anthropic, Google AI
|
|
315
|
+
case 'incomplete': // OpenAI Responses API - response cut short due to max tokens or content filter
|
|
316
|
+
return TLLMFinishReason.Length;
|
|
317
|
+
|
|
318
|
+
case 'model_context_window_exceeded': // Anthropic - context window exceeded
|
|
319
|
+
return TLLMFinishReason.ContextWindowLength;
|
|
320
|
+
|
|
321
|
+
// Content filtering and safety
|
|
322
|
+
case 'content_filter':
|
|
323
|
+
case 'contentfilter':
|
|
324
|
+
case 'refusal': // Anthropic - refused due to safety/policy
|
|
325
|
+
case 'safety': // Google AI - flagged by safety filters
|
|
326
|
+
case 'recitation': // Google AI - copyrighted content recitation
|
|
327
|
+
case 'language': // Google AI - unsupported language
|
|
328
|
+
case 'blocklist': // Google AI - forbidden terms
|
|
329
|
+
case 'prohibited_content': // Google AI - prohibited content
|
|
330
|
+
case 'spii': // Google AI - sensitive personally identifiable information
|
|
331
|
+
return TLLMFinishReason.ContentFilter;
|
|
332
|
+
|
|
333
|
+
// Tool/function calls
|
|
334
|
+
case 'tool_calls':
|
|
335
|
+
case 'tool_use': // Anthropic - tool invocation
|
|
336
|
+
case 'function_call': // OpenAI deprecated
|
|
337
|
+
return TLLMFinishReason.ToolCalls;
|
|
338
|
+
|
|
339
|
+
// Abort
|
|
340
|
+
case 'abort':
|
|
341
|
+
return TLLMFinishReason.Abort;
|
|
342
|
+
|
|
343
|
+
// Errors
|
|
344
|
+
case 'error':
|
|
345
|
+
case 'malformed_function_call': // Google AI - invalid function call
|
|
346
|
+
return TLLMFinishReason.Error;
|
|
347
|
+
|
|
348
|
+
// Unknown/unmapped
|
|
349
|
+
default:
|
|
350
|
+
return TLLMFinishReason.Unknown;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Gets a user-friendly error message based on the finish reason.
|
|
356
|
+
*
|
|
357
|
+
* @param finishReason - The normalized finish reason enum value
|
|
358
|
+
* @returns User-friendly error message explaining why the response was interrupted
|
|
359
|
+
*
|
|
360
|
+
* @example
|
|
361
|
+
* const message = LLMHelper.getFinishReasonErrorMessage(TLLMFinishReason.Length);
|
|
362
|
+
* console.log(message); // "Empty response. This is usually due to output token limit reached..."
|
|
363
|
+
*/
|
|
364
|
+
public static getFinishReasonErrorMessage(finishReason: TLLMFinishReason): string {
|
|
365
|
+
switch (finishReason) {
|
|
366
|
+
case TLLMFinishReason.Length:
|
|
367
|
+
return 'Empty response. This is usually due to output token limit reached. Please try again with a higher \'Maximum Output Tokens\'.';
|
|
368
|
+
|
|
369
|
+
case TLLMFinishReason.ContextWindowLength:
|
|
370
|
+
return 'Context window limit exceeded. Please shorten the input prompt or summarize the previous conversation to reduce token usage.';
|
|
371
|
+
|
|
372
|
+
case TLLMFinishReason.ContentFilter:
|
|
373
|
+
return 'The response was blocked by content filtering policies. Please modify your prompt and try again.';
|
|
374
|
+
|
|
375
|
+
case TLLMFinishReason.Abort:
|
|
376
|
+
return 'The request was aborted before completion.';
|
|
377
|
+
|
|
378
|
+
case TLLMFinishReason.Error:
|
|
379
|
+
return 'An error occurred while generating the response. Please try again.';
|
|
380
|
+
|
|
381
|
+
case TLLMFinishReason.ToolCalls:
|
|
382
|
+
return 'The model attempted to call a tool but the response was incomplete.';
|
|
383
|
+
|
|
384
|
+
case TLLMFinishReason.Unknown:
|
|
385
|
+
return 'The response was interrupted for an unknown reason. Please try again.';
|
|
386
|
+
|
|
387
|
+
case TLLMFinishReason.Stop:
|
|
388
|
+
default:
|
|
389
|
+
return 'The model stopped before completing the response.';
|
|
390
|
+
}
|
|
391
|
+
}
|
|
276
392
|
}
|
|
@@ -8,8 +8,9 @@ import { BinaryInput } from '@sre/helpers/BinaryInput.helper';
|
|
|
8
8
|
import { Logger } from '@sre/helpers/Log.helper';
|
|
9
9
|
import { AccessCandidate } from '@sre/Security/AccessControl/AccessCandidate.class';
|
|
10
10
|
import { IAgent } from '@sre/types/Agent.types';
|
|
11
|
-
import { TLLMChatResponse, TLLMMessageRole, TLLMModel, TLLMParams } from '@sre/types/LLM.types';
|
|
11
|
+
import { TLLMChatResponse, TLLMMessageRole, TLLMModel, TLLMParams, TLLMEvent, TLLMFinishReason } from '@sre/types/LLM.types';
|
|
12
12
|
|
|
13
|
+
import { LLMHelper } from './LLM.helper';
|
|
13
14
|
import { LLMConnector } from './LLM.service/LLMConnector';
|
|
14
15
|
import { IModelsProviderRequest, ModelsProviderConnector } from './ModelsProvider.service/ModelsProviderConnector';
|
|
15
16
|
|
|
@@ -96,29 +97,33 @@ export class LLMInference {
|
|
|
96
97
|
|
|
97
98
|
const result = this._llmConnector.postProcess(response?.content);
|
|
98
99
|
if (result.error) {
|
|
99
|
-
// If the model stopped before completing the response,
|
|
100
|
-
if (response.finishReason !==
|
|
101
|
-
|
|
100
|
+
// If the model stopped before completing the response normally, provide specific error message
|
|
101
|
+
if (response.finishReason !== TLLMFinishReason.Stop) {
|
|
102
|
+
const errorMessage = LLMHelper.getFinishReasonErrorMessage(response.finishReason);
|
|
103
|
+
throw new Error(errorMessage);
|
|
102
104
|
}
|
|
103
105
|
|
|
104
|
-
// If the model stopped
|
|
106
|
+
// If the model stopped normally but there's a postProcess error, throw the postProcess error
|
|
105
107
|
throw new Error(result.error);
|
|
106
108
|
}
|
|
107
109
|
return result;
|
|
108
110
|
} catch (error: any) {
|
|
109
111
|
// Attempt fallback for custom models (only if not already in fallback)
|
|
110
112
|
if (!isInFallback) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return
|
|
113
|
+
const isCustomModel = await this._modelProviderReq.isUserCustomLLM(this._model);
|
|
114
|
+
if (isCustomModel) {
|
|
115
|
+
try {
|
|
116
|
+
const fallbackParams = await this.getSafeFallbackParams(params);
|
|
117
|
+
const fallbackResult = await this.executeFallback('prompt', { query, contextWindow, files, params: fallbackParams, onFallback });
|
|
118
|
+
|
|
119
|
+
// If fallback succeeded, return the result
|
|
120
|
+
if (fallbackResult !== null) {
|
|
121
|
+
return fallbackResult;
|
|
122
|
+
}
|
|
123
|
+
} catch (fallbackError) {
|
|
124
|
+
// If fallback also failed, log it but continue to throw original error
|
|
125
|
+
logger.warn('Fallback also failed:', fallbackError);
|
|
118
126
|
}
|
|
119
|
-
} catch (fallbackError) {
|
|
120
|
-
// If fallback also failed, log it but continue to throw original error
|
|
121
|
-
logger.warn('Fallback also failed:', fallbackError);
|
|
122
127
|
}
|
|
123
128
|
}
|
|
124
129
|
|
|
@@ -147,41 +152,20 @@ export class LLMInference {
|
|
|
147
152
|
onFallback({ model: this._model });
|
|
148
153
|
}
|
|
149
154
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
} catch (error) {
|
|
153
|
-
// Attempt fallback for custom models (only if not already in fallback)
|
|
154
|
-
if (!isInFallback) {
|
|
155
|
-
try {
|
|
156
|
-
const fallbackParams = await this.getSafeFallbackParams(params);
|
|
157
|
-
const fallbackResult = await this.executeFallback('promptStream', {
|
|
158
|
-
query,
|
|
159
|
-
contextWindow,
|
|
160
|
-
files,
|
|
161
|
-
params: fallbackParams,
|
|
162
|
-
onFallback,
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
// If fallback succeeded, return the result
|
|
166
|
-
if (fallbackResult !== null) {
|
|
167
|
-
return fallbackResult;
|
|
168
|
-
}
|
|
169
|
-
} catch (fallbackError) {
|
|
170
|
-
// If fallback also failed, log it but continue to return error emitter
|
|
171
|
-
logger.warn('Fallback also failed:', fallbackError);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
155
|
+
// Connectors now always return emitters (they don't throw errors)
|
|
156
|
+
const primaryEmitter = await this._llmConnector.user(AccessCandidate.agent(params.agentId)).streamRequest(params);
|
|
174
157
|
|
|
175
|
-
|
|
176
|
-
|
|
158
|
+
// Only wrap with fallback capability if this is a custom model (not already in fallback)
|
|
159
|
+
// For regular models, return the emitter directly - errors flow naturally to the caller
|
|
160
|
+
if (!isInFallback) {
|
|
161
|
+
const isCustomModel = await this._modelProviderReq.isUserCustomLLM(this._model);
|
|
177
162
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
dummyEmitter.emit('end');
|
|
182
|
-
});
|
|
183
|
-
return dummyEmitter;
|
|
163
|
+
if (isCustomModel) {
|
|
164
|
+
return this.wrapWithFallback(primaryEmitter, { query, contextWindow, files, params, onFallback });
|
|
165
|
+
}
|
|
184
166
|
}
|
|
167
|
+
|
|
168
|
+
return primaryEmitter;
|
|
185
169
|
}
|
|
186
170
|
|
|
187
171
|
/**
|
|
@@ -232,19 +216,20 @@ export class LLMInference {
|
|
|
232
216
|
|
|
233
217
|
/**
|
|
234
218
|
* Executes fallback logic for custom models when the primary model fails.
|
|
235
|
-
*
|
|
219
|
+
* Checks if a fallback model is configured and switches to it.
|
|
236
220
|
* Prevents infinite loops by passing a flag to indicate we're in a fallback attempt.
|
|
237
221
|
*
|
|
222
|
+
* **Important**: This method should only be called for custom models (already verified by caller).
|
|
223
|
+
*
|
|
238
224
|
* @param methodName - The name of the method being called ('prompt' or 'promptStream')
|
|
239
225
|
* @param args - The original arguments passed to the method
|
|
240
|
-
* @returns The result from the fallback execution, or null if fallback
|
|
226
|
+
* @returns The result from the fallback execution, or null if no fallback is configured
|
|
241
227
|
*/
|
|
242
228
|
private async executeFallback(methodName: 'prompt' | 'promptStream', args: TPromptParams): Promise<any> {
|
|
243
|
-
const isCustomModel = await this._modelProviderReq.isUserCustomLLM(this._model);
|
|
244
229
|
const fallbackModel = await this._modelProviderReq.getFallbackLLM(this._model);
|
|
245
230
|
|
|
246
|
-
// Only execute fallback if
|
|
247
|
-
if (!
|
|
231
|
+
// Only execute fallback if a fallback model is configured
|
|
232
|
+
if (!fallbackModel) {
|
|
248
233
|
return null;
|
|
249
234
|
}
|
|
250
235
|
|
|
@@ -266,6 +251,93 @@ export class LLMInference {
|
|
|
266
251
|
}
|
|
267
252
|
}
|
|
268
253
|
|
|
254
|
+
/**
|
|
255
|
+
* Wraps an emitter with fallback capability using a proxy pattern.
|
|
256
|
+
* This creates a transparent proxy that forwards all events from the source emitter.
|
|
257
|
+
* On error, it attempts to switch to a fallback model and seamlessly redirects events.
|
|
258
|
+
*
|
|
259
|
+
* **Important**: This method is only called for custom models that have fallback configured.
|
|
260
|
+
* Regular models return their emitters directly without wrapping, so errors flow naturally.
|
|
261
|
+
*
|
|
262
|
+
* **Design Pattern**: Proxy/Decorator with listener-based event forwarding
|
|
263
|
+
* **Coupling**: Minimal - reads event types from TLLMEvent enum (single source of truth)
|
|
264
|
+
* **Reliability**: Uses listeners (not emit interception) to avoid timing issues with async emits
|
|
265
|
+
*
|
|
266
|
+
* Note: We use the TLLMEvent enum as the source of truth for all event types.
|
|
267
|
+
* This provides a good balance between decoupling and reliability. The enum already
|
|
268
|
+
* defines all possible LLM events, and connectors emit these standard events.
|
|
269
|
+
*
|
|
270
|
+
* @param sourceEmitter - The custom model's event emitter
|
|
271
|
+
* @param args - The original prompt arguments for fallback execution
|
|
272
|
+
* @returns A proxy emitter that transparently handles primary/fallback switching
|
|
273
|
+
*/
|
|
274
|
+
private wrapWithFallback(sourceEmitter: EventEmitter, args: TPromptParams): EventEmitter {
|
|
275
|
+
const proxyEmitter = new EventEmitter();
|
|
276
|
+
let fallbackAttempted = false;
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Attaches forwarding listeners for all event types in TLLMEvent.
|
|
280
|
+
* Uses listeners instead of emit() interception to avoid timing issues with setImmediate.
|
|
281
|
+
*
|
|
282
|
+
* @param source - The emitter to forward events from
|
|
283
|
+
* @param skipErrors - If true, skips forwarding error events (handled separately)
|
|
284
|
+
*/
|
|
285
|
+
const forwardAllEvents = (source: EventEmitter, skipErrors: boolean) => {
|
|
286
|
+
// Get all event types from TLLMEvent enum
|
|
287
|
+
const eventTypes = Object.values(TLLMEvent);
|
|
288
|
+
|
|
289
|
+
for (const eventType of eventTypes) {
|
|
290
|
+
// Skip error events if we're intercepting them
|
|
291
|
+
if (skipErrors && eventType === TLLMEvent.Error) {
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Attach listener to forward this event type
|
|
296
|
+
source.on(eventType, (...eventArgs: any[]) => {
|
|
297
|
+
proxyEmitter.emit(eventType, ...eventArgs);
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// Handle error events for fallback logic
|
|
303
|
+
const handleError = async (error: Error) => {
|
|
304
|
+
if (fallbackAttempted) return;
|
|
305
|
+
fallbackAttempted = true;
|
|
306
|
+
|
|
307
|
+
// Stop forwarding from primary emitter
|
|
308
|
+
sourceEmitter.removeAllListeners();
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
const fallbackParams = await this.getSafeFallbackParams(args.params);
|
|
312
|
+
const fallbackEmitter = await this.executeFallback('promptStream', {
|
|
313
|
+
...args,
|
|
314
|
+
params: fallbackParams,
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
if (fallbackEmitter) {
|
|
318
|
+
// Forward all events from fallback emitter (including errors)
|
|
319
|
+
forwardAllEvents(fallbackEmitter, false);
|
|
320
|
+
logger.info('Successfully switched to fallback stream');
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
} catch (fallbackError) {
|
|
324
|
+
logger.warn('Fallback attempt failed:', fallbackError);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// If we get here, fallback failed or was not available - emit error on proxy
|
|
328
|
+
proxyEmitter.emit(TLLMEvent.Error, error);
|
|
329
|
+
proxyEmitter.emit(TLLMEvent.End, [], [], TLLMFinishReason.Error);
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
// Attach error handler FIRST to intercept errors
|
|
333
|
+
sourceEmitter.once(TLLMEvent.Error, handleError);
|
|
334
|
+
|
|
335
|
+
// Forward all non-error events from primary emitter
|
|
336
|
+
forwardAllEvents(sourceEmitter, true);
|
|
337
|
+
|
|
338
|
+
return proxyEmitter;
|
|
339
|
+
}
|
|
340
|
+
|
|
269
341
|
public async imageGenRequest({ query, files, params }: TPromptParams) {
|
|
270
342
|
params.prompt = query;
|
|
271
343
|
return this._llmConnector.user(AccessCandidate.agent(params.agentId)).imageGenRequest(params);
|
|
@@ -280,24 +352,21 @@ export class LLMInference {
|
|
|
280
352
|
//@deprecated
|
|
281
353
|
public async streamRequest(params: any, agent: string | IAgent) {
|
|
282
354
|
const agentId = isAgent(agent) ? (agent as IAgent).id : agent;
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const model = params.model || this._model;
|
|
289
|
-
|
|
290
|
-
return await this._llmConnector.user(AccessCandidate.agent(agentId)).streamRequest({ ...params, model });
|
|
291
|
-
} catch (error) {
|
|
292
|
-
logger.error('Error in streamRequest:', error);
|
|
293
|
-
|
|
294
|
-
const dummyEmitter = new EventEmitter();
|
|
355
|
+
if (!params.messages || !params.messages?.length) {
|
|
356
|
+
// Return an emitter with error/end events for validation errors
|
|
357
|
+
const errorEmitter = new EventEmitter();
|
|
358
|
+
const validationError = new Error('Input messages are required.');
|
|
295
359
|
process.nextTick(() => {
|
|
296
|
-
|
|
297
|
-
|
|
360
|
+
errorEmitter.emit(TLLMEvent.Error, validationError);
|
|
361
|
+
errorEmitter.emit(TLLMEvent.End, [], [], TLLMFinishReason.Error);
|
|
298
362
|
});
|
|
299
|
-
return
|
|
363
|
+
return errorEmitter;
|
|
300
364
|
}
|
|
365
|
+
|
|
366
|
+
const model = params.model || this._model;
|
|
367
|
+
|
|
368
|
+
// Connectors now always return emitters (they don't throw errors)
|
|
369
|
+
return await this._llmConnector.user(AccessCandidate.agent(agentId)).streamRequest({ ...params, model });
|
|
301
370
|
}
|
|
302
371
|
|
|
303
372
|
//@deprecated
|
|
@@ -112,6 +112,7 @@ export abstract class LLMConnector extends Connector {
|
|
|
112
112
|
const response = await this.request({
|
|
113
113
|
acRequest: candidate.readRequest,
|
|
114
114
|
body: preparedParams.body,
|
|
115
|
+
abortSignal: preparedParams.abortSignal,
|
|
115
116
|
context: {
|
|
116
117
|
modelEntryName: preparedParams.modelEntryName,
|
|
117
118
|
agentId: preparedParams.agentId,
|
|
@@ -137,6 +138,7 @@ export abstract class LLMConnector extends Connector {
|
|
|
137
138
|
const requestParams = {
|
|
138
139
|
acRequest: candidate.readRequest,
|
|
139
140
|
body: preparedParams.body,
|
|
141
|
+
abortSignal: preparedParams.abortSignal,
|
|
140
142
|
context: {
|
|
141
143
|
modelEntryName: preparedParams.modelEntryName,
|
|
142
144
|
agentId: preparedParams.agentId,
|
|
@@ -262,15 +264,18 @@ export abstract class LLMConnector extends Connector {
|
|
|
262
264
|
|
|
263
265
|
private async prepareParams(candidate: AccessCandidate, params: TLLMConnectorParams): Promise<TLLMPreparedParams> {
|
|
264
266
|
const modelsProvider: ModelsProviderConnector = ConnectorService.getModelsProviderConnector();
|
|
265
|
-
//
|
|
266
|
-
const files = params
|
|
267
|
-
delete params?.files; // need to remove files to avoid any issues during JSON.stringify() especially when we have large files
|
|
267
|
+
// Extract files and abortSignal from the original parameters to avoid overwriting the original constructor
|
|
268
|
+
const { files, abortSignal, ...restParams } = params;
|
|
268
269
|
|
|
269
|
-
const clonedParams = JSON.parse(JSON.stringify(
|
|
270
|
+
const clonedParams = JSON.parse(JSON.stringify(restParams)); // Avoid mutation of the original params
|
|
270
271
|
|
|
271
272
|
// Format the parameters to ensure proper type of values
|
|
272
273
|
const _params: TLLMPreparedParams = this.formatParamValues(clonedParams);
|
|
273
274
|
|
|
275
|
+
// Re-attach non-serializable properties ignored before cloning
|
|
276
|
+
_params.abortSignal = abortSignal;
|
|
277
|
+
_params.files = files;
|
|
278
|
+
|
|
274
279
|
const model = _params.model;
|
|
275
280
|
const teamId = await this.getTeamId(candidate);
|
|
276
281
|
|
|
@@ -307,8 +312,6 @@ export abstract class LLMConnector extends Connector {
|
|
|
307
312
|
}
|
|
308
313
|
|
|
309
314
|
_params.model = await modelProviderCandidate.getModelId(model);
|
|
310
|
-
// Attach the files again after formatting the parameters
|
|
311
|
-
_params.files = files;
|
|
312
315
|
|
|
313
316
|
const features = modelInfo?.features || [];
|
|
314
317
|
|
|
@@ -327,6 +330,9 @@ export abstract class LLMConnector extends Connector {
|
|
|
327
330
|
xai: await this.prepareXAIToolsInfo(_params),
|
|
328
331
|
};
|
|
329
332
|
|
|
333
|
+
// Filter out default and system-specific outputs (e.g., _debug, _error) to isolate custom outputs for structured response
|
|
334
|
+
_params.structuredOutputs = _params?.outputs?.filter((output) => !output.default && !['_debug', '_error'].includes(output.name)) || [];
|
|
335
|
+
|
|
330
336
|
// The input adapter transforms the standardized parameters into the specific format required by the target LLM provider
|
|
331
337
|
_params.agentId = candidate.id;
|
|
332
338
|
const body = await this.reqBodyAdapter(_params);
|
|
@@ -461,6 +467,7 @@ export abstract class LLMConnector extends Connector {
|
|
|
461
467
|
}
|
|
462
468
|
|
|
463
469
|
//FIXME: to revisit by Alaa-eddine
|
|
470
|
+
// TODO: This part is a bit confusing. We send “consistent” messages to the LLM, but they still aren’t truly consistent. For example, we send { role: 'system', content: 'You are a helpful assistant.' }, which isn’t compatible with Google AI. However, we still need to mark it as `system` because we later convert it to `systemInstruction`. We should revisit the architecture later and make the flow simpler and more straightforward.
|
|
464
471
|
if (key === 'messages') {
|
|
465
472
|
_value = this.getConsistentMessages(_value);
|
|
466
473
|
}
|