@futdevpro/nts-dynamo 1.15.36 → 1.15.37
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/.dynamo/logs/cicd-pipeline/output.log +1522 -1485
- package/.dynamo/logs/cicd-pipeline/status.json +181 -181
- package/build/_modules/ai/_modules/open-ai/_services/oai-llm.service-base.d.ts +24 -2
- package/build/_modules/ai/_modules/open-ai/_services/oai-llm.service-base.d.ts.map +1 -1
- package/build/_modules/ai/_modules/open-ai/_services/oai-llm.service-base.js +124 -0
- package/build/_modules/ai/_modules/open-ai/_services/oai-llm.service-base.js.map +1 -1
- package/build/_modules/ai/_services/ai-llm.service-base.d.ts +56 -1
- package/build/_modules/ai/_services/ai-llm.service-base.d.ts.map +1 -1
- package/build/_modules/ai/_services/ai-llm.service-base.js +122 -0
- package/build/_modules/ai/_services/ai-llm.service-base.js.map +1 -1
- package/package.json +2 -2
- package/pnpm-workspace.yaml +3 -0
- package/src/_modules/ai/_modules/open-ai/_services/oai-llm.service-base.tools.spec.ts +106 -0
- package/src/_modules/ai/_modules/open-ai/_services/oai-llm.service-base.ts +154 -5
- package/src/_modules/ai/_services/ai-llm.service-base.ts +180 -2
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
import { OpenAI } from 'openai';
|
|
2
2
|
|
|
3
|
-
import { DyFM_OAI_Settings, DyFM_OAI_Model, DyFM_OAI_CallSettings } from '@futdevpro/fsm-dynamo/ai/open-ai';
|
|
3
|
+
import { DyFM_OAI_Settings, DyFM_OAI_Model, DyFM_OAI_CallSettings, DyFM_OAI_Models } from '@futdevpro/fsm-dynamo/ai/open-ai';
|
|
4
4
|
import { DyFM_AnyError, DyFM_Error, DyFM_Error_Settings, DyFM_Log, DyFM_notNull, DyFM_Object } from '@futdevpro/fsm-dynamo';
|
|
5
|
-
import {
|
|
6
|
-
DyFM_AI_Message,
|
|
5
|
+
import {
|
|
6
|
+
DyFM_AI_Message,
|
|
7
7
|
DyFM_AI_MessageRole,
|
|
8
8
|
DyFM_AI_Provider,
|
|
9
9
|
DyFM_AI_ProviderCapabilities,
|
|
10
10
|
DyFM_AI_CallSettings,
|
|
11
11
|
DyFM_AI_Config,
|
|
12
|
-
DyFM_AI_LLM_Response
|
|
12
|
+
DyFM_AI_LLM_Response,
|
|
13
|
+
DyFM_AI_Tool,
|
|
14
|
+
DyFM_AI_ToolCall,
|
|
15
|
+
DyFM_AI_ModelInfo
|
|
13
16
|
} from '@futdevpro/fsm-dynamo/ai';
|
|
14
17
|
|
|
15
18
|
import { DyNTS_global_settings } from '../../../../../_collections/global-settings.const';
|
|
16
19
|
import { ChatCompletion } from 'openai/resources';
|
|
17
|
-
import { ChatCompletionCreateParamsBase, ChatCompletionMessageParam } from 'openai/resources/chat/completions';
|
|
20
|
+
import { ChatCompletionCreateParamsBase, ChatCompletionMessageParam, ChatCompletionTool } from 'openai/resources/chat/completions';
|
|
18
21
|
import { DyNTS_AI_CostEventCallback } from '../../../_models/interfaces/dynts-ai-cost-event-callback.interface';
|
|
19
22
|
import { DyNTS_OAI_LLM_Predefined_Requests } from '../_models/interfaces/oai-llm-predefined-requests.interface';
|
|
20
23
|
import { DyNTS_OAI_global_settings } from '../_collections/oai-global-settings.const';
|
|
@@ -567,6 +570,152 @@ export class DyNTS_OAI_LLM_ServiceBase extends DyNTS_AI_LLM_ServiceBase {
|
|
|
567
570
|
}
|
|
568
571
|
}
|
|
569
572
|
|
|
573
|
+
//#region Function calling (tool use) — FR-047
|
|
574
|
+
|
|
575
|
+
/** OpenAI tool-kepes modell-registry (a base capability-precheck-jehez). */
|
|
576
|
+
protected override getModelRegistry(): DyFM_AI_ModelInfo[] {
|
|
577
|
+
return DyFM_OAI_Models;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Egy OpenAI-kor a tool-loop-ban: felepiti a natív request-et (system + a teljes beszelgetes
|
|
582
|
+
* tool-uzenetekkel + tools), meghivja az API-t, es normalizalt DyFM_AI_LLM_Response-t ad vissza
|
|
583
|
+
* (toolCalls feltoltve, ha a model tool-t akar hivni). FR-002 cost-event: 'llm-tool-use'.
|
|
584
|
+
*/
|
|
585
|
+
protected override async callModelWithTools(
|
|
586
|
+
set: {
|
|
587
|
+
conversation: DyFM_AI_Message[];
|
|
588
|
+
tools: DyFM_AI_Tool[];
|
|
589
|
+
settings?: DyFM_OAI_CallSettings;
|
|
590
|
+
issuer: string;
|
|
591
|
+
}
|
|
592
|
+
): Promise<DyFM_AI_LLM_Response> {
|
|
593
|
+
try {
|
|
594
|
+
const settings: DyFM_OAI_CallSettings = set.settings ?? this.defaultSettings;
|
|
595
|
+
|
|
596
|
+
const messages: ChatCompletionMessageParam[] = [
|
|
597
|
+
this.toOpenAIMessage(this.getDefaultSystemMessage(settings)),
|
|
598
|
+
...set.conversation.map((message: DyFM_AI_Message) => this.toOpenAIMessage(message)),
|
|
599
|
+
];
|
|
600
|
+
|
|
601
|
+
const input: ChatCompletionCreateParamsBase = {
|
|
602
|
+
model: settings?.useModel ?? this.defaultModel,
|
|
603
|
+
messages: messages,
|
|
604
|
+
temperature: DyFM_notNull(settings?.temperature) ? settings.temperature : this.defaultSettings.temperature,
|
|
605
|
+
max_completion_tokens: DyFM_notNull(settings?.maxTokens) ? settings.maxTokens : this.defaultSettings.maxTokens,
|
|
606
|
+
top_p: DyFM_notNull(settings?.topP) ? settings.topP : this.defaultSettings.topP,
|
|
607
|
+
frequency_penalty: DyFM_notNull(settings?.frequencyPenalty) ? settings.frequencyPenalty : this.defaultSettings.frequencyPenalty,
|
|
608
|
+
presence_penalty: DyFM_notNull(settings?.presencePenalty) ? settings.presencePenalty : this.defaultSettings.presencePenalty,
|
|
609
|
+
tools: set.tools.map((tool: DyFM_AI_Tool) => this.toOpenAITool(tool)),
|
|
610
|
+
tool_choice: 'auto',
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
const start: number = Date.now();
|
|
614
|
+
const result: ChatCompletion = await this.openai.chat.completions.create(input) as ChatCompletion;
|
|
615
|
+
const durationMs: number = Date.now() - start;
|
|
616
|
+
|
|
617
|
+
const message = result.choices[0].message;
|
|
618
|
+
|
|
619
|
+
const toolCalls: DyFM_AI_ToolCall[] = (message.tool_calls ?? [])
|
|
620
|
+
.filter((toolCall) => toolCall.type === 'function')
|
|
621
|
+
.map((toolCall) => ({
|
|
622
|
+
id: toolCall.id,
|
|
623
|
+
name: toolCall.function.name,
|
|
624
|
+
arguments: this.parseToolArguments(toolCall.function.arguments),
|
|
625
|
+
}));
|
|
626
|
+
|
|
627
|
+
this.emitCostEvent({
|
|
628
|
+
callType: 'llm-tool-use',
|
|
629
|
+
provider: this.aiProvider,
|
|
630
|
+
model: String(input.model ?? this.defaultModel),
|
|
631
|
+
tokensUsed: {
|
|
632
|
+
input: result.usage?.prompt_tokens ?? 0,
|
|
633
|
+
output: result.usage?.completion_tokens ?? 0,
|
|
634
|
+
total: result.usage?.total_tokens ?? 0,
|
|
635
|
+
},
|
|
636
|
+
durationMs: durationMs,
|
|
637
|
+
issuer: set.issuer,
|
|
638
|
+
timestamp: new Date(),
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
return {
|
|
642
|
+
content: message.content ?? '',
|
|
643
|
+
toolCalls: toolCalls.length ? toolCalls : undefined,
|
|
644
|
+
usage: result.usage ? {
|
|
645
|
+
promptTokens: result.usage.prompt_tokens,
|
|
646
|
+
completionTokens: result.usage.completion_tokens,
|
|
647
|
+
totalTokens: result.usage.total_tokens,
|
|
648
|
+
} : undefined,
|
|
649
|
+
model: result.model,
|
|
650
|
+
finishReason: result.choices[0].finish_reason,
|
|
651
|
+
stopReason: result.choices[0].finish_reason,
|
|
652
|
+
rawResponse: result,
|
|
653
|
+
};
|
|
654
|
+
} catch (error) {
|
|
655
|
+
throw new DyFM_Error({
|
|
656
|
+
...this.getDefaultErrorSettings('callModelWithTools', error, set.issuer),
|
|
657
|
+
errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-OLSB-TC0`,
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/** Agnosztikus tool-definicio → OpenAI ChatCompletionTool. */
|
|
663
|
+
protected toOpenAITool(tool: DyFM_AI_Tool): ChatCompletionTool {
|
|
664
|
+
return {
|
|
665
|
+
type: 'function',
|
|
666
|
+
function: {
|
|
667
|
+
name: tool.name,
|
|
668
|
+
description: tool.description,
|
|
669
|
+
parameters: tool.parameters as Record<string, unknown>,
|
|
670
|
+
},
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Agnosztikus DyFM_AI_Message → OpenAI ChatCompletionMessageParam.
|
|
676
|
+
* Kezeli a tool-result (role:'tool') es a tool-hivo assistant (tool_calls) uzeneteket is.
|
|
677
|
+
*/
|
|
678
|
+
protected toOpenAIMessage(message: DyFM_AI_Message): ChatCompletionMessageParam {
|
|
679
|
+
if (message.role === DyFM_AI_MessageRole.tool) {
|
|
680
|
+
return {
|
|
681
|
+
role: 'tool',
|
|
682
|
+
tool_call_id: message.toolCallId ?? '',
|
|
683
|
+
content: message.content,
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (message.role === DyFM_AI_MessageRole.assistant && message.toolCalls?.length) {
|
|
688
|
+
return {
|
|
689
|
+
role: 'assistant',
|
|
690
|
+
content: message.content || null,
|
|
691
|
+
tool_calls: message.toolCalls.map((toolCall: DyFM_AI_ToolCall) => ({
|
|
692
|
+
id: toolCall.id,
|
|
693
|
+
type: 'function' as const,
|
|
694
|
+
function: {
|
|
695
|
+
name: toolCall.name,
|
|
696
|
+
arguments: JSON.stringify(toolCall.arguments),
|
|
697
|
+
},
|
|
698
|
+
})),
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
return {
|
|
703
|
+
role: message.role as 'system' | 'user' | 'assistant',
|
|
704
|
+
content: message.content,
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/** Tool-argumentumok parse-olasa (OpenAI JSON-string → object); hibas JSON eseten ures object. */
|
|
709
|
+
protected parseToolArguments(raw: string): Record<string, unknown> {
|
|
710
|
+
try {
|
|
711
|
+
return JSON.parse(raw || '{}');
|
|
712
|
+
} catch {
|
|
713
|
+
return {};
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
//#endregion
|
|
718
|
+
|
|
570
719
|
// async askJSONListQuestion(
|
|
571
720
|
// question: string,
|
|
572
721
|
// issuer: string,
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { DyNTS_AI_Provider_ServiceBase } from './ai-provider.service-base';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
DyFM_AI_CallSettings,
|
|
4
|
+
DyFM_AI_Message,
|
|
5
|
+
DyFM_AI_LLM_Response,
|
|
6
|
+
DyFM_AI_MessageRole,
|
|
7
|
+
DyFM_AI_Tool,
|
|
8
|
+
DyFM_AI_ToolCall,
|
|
9
|
+
DyFM_AI_ToolResult,
|
|
10
|
+
DyFM_AI_ToolHandlers,
|
|
11
|
+
DyFM_AI_ModelInfo,
|
|
12
|
+
DyFM_AI_ModelRegistry_Util,
|
|
13
|
+
} from '@futdevpro/fsm-dynamo/ai';
|
|
3
14
|
import { DyFM_Error, DyFM_Error_Settings, DyFM_getLocalStackLocation, DyFM_Log, DyFM_Object } from '@futdevpro/fsm-dynamo';
|
|
4
15
|
import {
|
|
5
16
|
DyFM_AI_GenericSelect_Input,
|
|
@@ -40,7 +51,174 @@ export abstract class DyNTS_AI_LLM_ServiceBase<
|
|
|
40
51
|
}
|
|
41
52
|
|
|
42
53
|
defaultLogReplacer: string = '...long-context...';
|
|
43
|
-
|
|
54
|
+
|
|
55
|
+
//////////////////////////////////////////////////////////////////////////////////////////
|
|
56
|
+
// FUNCTION CALLING (TOOL USE) — FR-047 //
|
|
57
|
+
//////////////////////////////////////////////////////////////////////////////////////////
|
|
58
|
+
// Provider-agnostic agent-loop. The loop logic lives here ONCE (ported from the legacy
|
|
59
|
+
// FDPNTS_GPT_ControlService.getAnswerWithTools); each provider only overrides
|
|
60
|
+
// `callModelWithTools` (one provider turn) and `getModelRegistry` (capability honesty).
|
|
61
|
+
// Tools are a REQUEST parameter (not on settings), per the FR-047 design.
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* A provider tool-kepes modell-registry-je (override providerenkent — pl. DyFM_OAI_Models).
|
|
65
|
+
* Ures default → a modell tool-kepessege ismeretlen → a precheck elutasitja (honesty).
|
|
66
|
+
*/
|
|
67
|
+
protected getModelRegistry(): DyFM_AI_ModelInfo[] {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Ellenorzi, hogy a feloldott modell tamogatja-e a function calling-ot; ha nem, beszedes
|
|
73
|
+
* hibaval elhasal MIELOTT barmilyen API-hivas tortenne (kritikus pl. a Local providernel).
|
|
74
|
+
*/
|
|
75
|
+
protected assertToolsSupported(modelId: string): void {
|
|
76
|
+
if (!DyFM_AI_ModelRegistry_Util.modelSupportsTools(this.getModelRegistry(), modelId)) {
|
|
77
|
+
throw new DyFM_Error({
|
|
78
|
+
message: `Model '${modelId}' does not support function calling (provider: ${this.aiProvider})`,
|
|
79
|
+
userMessage: `The selected AI model does not support tools.`,
|
|
80
|
+
errorCode: 'DyNTS-AILSB-TLC0',
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* GPT valasz Function Calling Agent Tool-okkal — provider-agnosztikus agent-loop.
|
|
87
|
+
* @description A `tools` definiciok + `toolHandlers` alapjan tool-loop-ot futtat: hivja a
|
|
88
|
+
* modellt (callModelWithTools) → ha a valaszban tool-call van, lefuttatja a regisztralt
|
|
89
|
+
* handler-t (runToolCall, never-throw) es az eredmenyt visszafuzi → ismetli, amig a model
|
|
90
|
+
* vegso (tool-call nelkuli) valaszt ad, vagy a maxIterations limitet eleri. A tool-ok a
|
|
91
|
+
* REQUEST-parameterben jonnek, nem a settings-ben.
|
|
92
|
+
*/
|
|
93
|
+
async requestWithTools(
|
|
94
|
+
set: {
|
|
95
|
+
conversation: DyFM_AI_Message[];
|
|
96
|
+
tools: DyFM_AI_Tool[];
|
|
97
|
+
toolHandlers: DyFM_AI_ToolHandlers;
|
|
98
|
+
settings?: T_AISettings;
|
|
99
|
+
issuer: string;
|
|
100
|
+
maxIterations?: number;
|
|
101
|
+
}
|
|
102
|
+
): Promise<DyFM_AI_LLM_Response> {
|
|
103
|
+
const modelId: string = (set.settings?.useModel ?? this.defaultModel) as string;
|
|
104
|
+
this.assertToolsSupported(modelId);
|
|
105
|
+
|
|
106
|
+
return this.runToolLoop(set);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Egy provider-kor a tool-loop-ban: elkuldi a beszelgetest + tool-okat, normalizalt valaszt ad.
|
|
111
|
+
* @description Override-olando providerenkent (OpenAI / Anthropic / Google / …). A default
|
|
112
|
+
* elhasal — egy provider, ami nem implementalja, tool-use-t nem tud kiszolgalni.
|
|
113
|
+
*/
|
|
114
|
+
protected async callModelWithTools(
|
|
115
|
+
_set: {
|
|
116
|
+
conversation: DyFM_AI_Message[];
|
|
117
|
+
tools: DyFM_AI_Tool[];
|
|
118
|
+
settings?: T_AISettings;
|
|
119
|
+
issuer: string;
|
|
120
|
+
}
|
|
121
|
+
): Promise<DyFM_AI_LLM_Response> {
|
|
122
|
+
throw new DyFM_Error({
|
|
123
|
+
message: `callModelWithTools is not implemented for provider '${this.aiProvider}'`,
|
|
124
|
+
userMessage: `Function calling is not available for this AI provider yet.`,
|
|
125
|
+
errorCode: 'DyNTS-AILSB-TLN0',
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* A provider-agnosztikus agent-loop torzse. A bemeno `conversation` ele a provider teszi a
|
|
131
|
+
* system-message-et (callModelWithTools); a loop a tool-call/tool-result uzeneteket fuzi hozza.
|
|
132
|
+
*/
|
|
133
|
+
protected async runToolLoop(
|
|
134
|
+
set: {
|
|
135
|
+
conversation: DyFM_AI_Message[];
|
|
136
|
+
tools: DyFM_AI_Tool[];
|
|
137
|
+
toolHandlers: DyFM_AI_ToolHandlers;
|
|
138
|
+
settings?: T_AISettings;
|
|
139
|
+
issuer: string;
|
|
140
|
+
maxIterations?: number;
|
|
141
|
+
}
|
|
142
|
+
): Promise<DyFM_AI_LLM_Response> {
|
|
143
|
+
const maxIterations: number = set.maxIterations ?? 8;
|
|
144
|
+
const conversation: DyFM_AI_Message[] = [...set.conversation];
|
|
145
|
+
|
|
146
|
+
for (let iteration: number = 0; iteration < maxIterations; iteration++) {
|
|
147
|
+
const response: DyFM_AI_LLM_Response = await this.callModelWithTools({
|
|
148
|
+
conversation: conversation,
|
|
149
|
+
tools: set.tools,
|
|
150
|
+
settings: set.settings,
|
|
151
|
+
issuer: set.issuer,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// nincs tool-hivas → ez a vegso valasz
|
|
155
|
+
if (!response.toolCalls?.length) {
|
|
156
|
+
return response;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// a model tool-hivo (assistant) uzenetet visszatesszuk a kontextusba
|
|
160
|
+
conversation.push({
|
|
161
|
+
role: DyFM_AI_MessageRole.assistant,
|
|
162
|
+
content: response.content ?? '',
|
|
163
|
+
toolCalls: response.toolCalls,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// minden tool-hivast lefuttatunk (never-throw), es az eredmenyt visszaadjuk a modellnek
|
|
167
|
+
const results: DyFM_AI_ToolResult[] = await Promise.all(
|
|
168
|
+
response.toolCalls.map((call: DyFM_AI_ToolCall) => this.runToolCall(call, set.toolHandlers))
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
results.forEach((result: DyFM_AI_ToolResult) => {
|
|
172
|
+
conversation.push({
|
|
173
|
+
role: DyFM_AI_MessageRole.tool,
|
|
174
|
+
content: result.content,
|
|
175
|
+
toolCallId: result.toolCallId,
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// a tool-loop nem konvergalt a limiten belul
|
|
181
|
+
throw new DyFM_Error({
|
|
182
|
+
message: `Tool loop did not converge within ${maxIterations} iterations`,
|
|
183
|
+
userMessage: `We encountered an error while running AI tools, please contact the responsible development team.`,
|
|
184
|
+
errorCode: 'DyNTS-AILSB-TL0',
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Egy tool-hivas lefuttatasa a regisztralt handler-rel. SOHA nem dob — a tool-hiba string-kent
|
|
190
|
+
* megy vissza a modellnek (hogy korrigalhasson), hianyzo handler eseten is informativ uzenet
|
|
191
|
+
* (soha nem [object Object]).
|
|
192
|
+
*/
|
|
193
|
+
protected async runToolCall(
|
|
194
|
+
call: DyFM_AI_ToolCall,
|
|
195
|
+
toolHandlers: DyFM_AI_ToolHandlers
|
|
196
|
+
): Promise<DyFM_AI_ToolResult> {
|
|
197
|
+
const handler = toolHandlers[call.name];
|
|
198
|
+
|
|
199
|
+
if (!handler) {
|
|
200
|
+
return {
|
|
201
|
+
toolCallId: call.id,
|
|
202
|
+
content: `ERROR: no handler registered for tool '${call.name}'`,
|
|
203
|
+
isError: true,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
return {
|
|
209
|
+
toolCallId: call.id,
|
|
210
|
+
content: await handler(call.arguments),
|
|
211
|
+
};
|
|
212
|
+
} catch (error) {
|
|
213
|
+
return {
|
|
214
|
+
toolCallId: call.id,
|
|
215
|
+
content: `ERROR executing tool '${call.name}': ` +
|
|
216
|
+
`${error instanceof Error ? error.message : String(error)}`,
|
|
217
|
+
isError: true,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
44
222
|
// Core abstract methods
|
|
45
223
|
/**
|
|
46
224
|
* Call LLM with system and user messages
|