@llmbridge/plugin-logging 0.1.1 → 0.1.3

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/dist/index.d.mts CHANGED
@@ -15,14 +15,24 @@ declare function generateRandomDigits(length: number): string;
15
15
  declare class LoggingPlugin implements LLMBridge.Plugin<void, string, void, void> {
16
16
  private options;
17
17
  private logger;
18
+ private runSessions;
18
19
  constructor(options: LoggingPluginOptions);
19
20
  private log;
20
21
  wrapRun(next: () => Promise<LLMBridge.Response>, context: LLMBridge.PluginCompletionContext): Promise<LLMBridge.Response>;
21
22
  wrapExec(next: (params: any) => Promise<any>, params: any, context: LLMBridge.PluginCompletionContext): Promise<any>;
22
23
  wrapToolExec(next: (tool: LLMBridge.Tool, counter: number, input: any) => Promise<any>, tool: LLMBridge.Tool, counter: number, input: any, context: LLMBridge.PluginCompletionContext): Promise<any>;
23
- createLogFiles(requestId: string, type: 'request' | 'response', data: any): void;
24
- createTxtFile(filePath: string, data: any): void;
25
- objectToTxt(obj: any, prefix?: string): string;
24
+ private writeJsonLog;
25
+ private writeSessionXml;
26
+ private logError;
27
+ private getDailyFolder;
28
+ private buildSessionXml;
29
+ private parseArguments;
30
+ private appendCdata;
31
+ private appendArguments;
32
+ private appendToolCalls;
33
+ private appendRawArguments;
34
+ private sanitizeAttribute;
35
+ private cleanXmlString;
26
36
  }
27
37
 
28
38
  export { type Logger, LoggingPlugin, type LoggingPluginOptions, generateRandomDigits };
package/dist/index.d.ts CHANGED
@@ -15,14 +15,24 @@ declare function generateRandomDigits(length: number): string;
15
15
  declare class LoggingPlugin implements LLMBridge.Plugin<void, string, void, void> {
16
16
  private options;
17
17
  private logger;
18
+ private runSessions;
18
19
  constructor(options: LoggingPluginOptions);
19
20
  private log;
20
21
  wrapRun(next: () => Promise<LLMBridge.Response>, context: LLMBridge.PluginCompletionContext): Promise<LLMBridge.Response>;
21
22
  wrapExec(next: (params: any) => Promise<any>, params: any, context: LLMBridge.PluginCompletionContext): Promise<any>;
22
23
  wrapToolExec(next: (tool: LLMBridge.Tool, counter: number, input: any) => Promise<any>, tool: LLMBridge.Tool, counter: number, input: any, context: LLMBridge.PluginCompletionContext): Promise<any>;
23
- createLogFiles(requestId: string, type: 'request' | 'response', data: any): void;
24
- createTxtFile(filePath: string, data: any): void;
25
- objectToTxt(obj: any, prefix?: string): string;
24
+ private writeJsonLog;
25
+ private writeSessionXml;
26
+ private logError;
27
+ private getDailyFolder;
28
+ private buildSessionXml;
29
+ private parseArguments;
30
+ private appendCdata;
31
+ private appendArguments;
32
+ private appendToolCalls;
33
+ private appendRawArguments;
34
+ private sanitizeAttribute;
35
+ private cleanXmlString;
26
36
  }
27
37
 
28
38
  export { type Logger, LoggingPlugin, type LoggingPluginOptions, generateRandomDigits };
package/dist/index.js CHANGED
@@ -36,15 +36,17 @@ __export(index_exports, {
36
36
  module.exports = __toCommonJS(index_exports);
37
37
 
38
38
  // src/logging.ts
39
- var import_path = __toESM(require("path"));
40
- var import_fs = __toESM(require("fs"));
41
- var import_crypto = require("crypto");
39
+ var import_node_path = __toESM(require("path"));
40
+ var import_node_fs = __toESM(require("fs"));
41
+ var import_node_crypto = require("crypto");
42
42
  var import_moment = __toESM(require("moment"));
43
+ var import_xmlbuilder2 = require("xmlbuilder2");
43
44
  function generateRandomDigits(length) {
44
- return (0, import_crypto.randomBytes)(Math.ceil(length / 2)).toString("hex").slice(0, length);
45
+ return (0, import_node_crypto.randomBytes)(Math.ceil(length / 2)).toString("hex").slice(0, length);
45
46
  }
46
47
  var LoggingPlugin = class {
47
48
  constructor(options) {
49
+ this.runSessions = /* @__PURE__ */ new Map();
48
50
  this.options = options;
49
51
  this.logger = options.logger;
50
52
  }
@@ -59,60 +61,239 @@ var LoggingPlugin = class {
59
61
  }
60
62
  async wrapRun(next, context) {
61
63
  this.log(`run ${context.model}`);
62
- const response = await next();
63
- const lastResponsePath = import_path.default.join(this.options.folder, "last_response.json");
64
- import_fs.default.writeFileSync(lastResponsePath, JSON.stringify(response, null, 2));
65
- return response;
64
+ const session = {
65
+ startTime: (0, import_moment.default)(),
66
+ requestId: generateRandomDigits(4),
67
+ model: context.model,
68
+ turns: []
69
+ };
70
+ this.runSessions.set(context, session);
71
+ try {
72
+ const response = await next();
73
+ const lastResponsePath = import_node_path.default.join(this.options.folder, "last_response.json");
74
+ import_node_fs.default.writeFileSync(lastResponsePath, JSON.stringify(response, null, 2));
75
+ this.writeSessionXml(session);
76
+ return response;
77
+ } catch (error) {
78
+ this.writeSessionXml(session);
79
+ throw error;
80
+ } finally {
81
+ this.runSessions.delete(context);
82
+ }
66
83
  }
67
84
  async wrapExec(next, params, context) {
68
85
  const requestId = generateRandomDigits(4);
69
- this.createLogFiles(requestId, "request", params);
86
+ const requestTime = (0, import_moment.default)();
87
+ const session = this.runSessions.get(context);
88
+ const turn = { request: params, response: void 0, toolExecutions: [] };
89
+ this.writeJsonLog(requestTime, requestId, "request", params);
90
+ if (session) {
91
+ session.turns.push(turn);
92
+ this.writeSessionXml(session);
93
+ }
70
94
  const response = await next(params);
71
- this.createLogFiles(requestId, "response", response);
95
+ this.writeJsonLog((0, import_moment.default)(), requestId, "response", response);
96
+ if (session) {
97
+ turn.response = response;
98
+ this.writeSessionXml(session);
99
+ }
72
100
  return response;
73
101
  }
74
102
  async wrapToolExec(next, tool, counter, input, context) {
75
103
  this.log(`run (${counter + 1}/${context.options.tools?.usesLimit}) tool ${tool.name} ${JSON.stringify(input)}`);
76
104
  const result = await next(tool, counter, input);
77
105
  this.log(`tool response ${JSON.stringify(result)}`);
106
+ const session = this.runSessions.get(context);
107
+ if (session && session.turns.length > 0) {
108
+ const lastTurn = session.turns[session.turns.length - 1];
109
+ lastTurn.toolExecutions.push({ toolName: tool.name, counter, input, result });
110
+ this.writeSessionXml(session);
111
+ }
78
112
  return result;
79
113
  }
80
- createLogFiles(requestId, type, data) {
81
- const currentDate = (0, import_moment.default)();
82
- const dateString = currentDate.format("YYYY_MM_DD");
83
- const timeString = currentDate.format("YYYY_MM_DD_HH_mm_ss");
84
- const dailyDir = import_path.default.join(this.options.folder, dateString);
85
- if (!import_fs.default.existsSync(dailyDir)) {
86
- import_fs.default.mkdirSync(dailyDir, { recursive: true });
87
- }
114
+ writeJsonLog(date, requestId, type, data) {
115
+ const { dailyDir, timeString } = this.getDailyFolder(date);
88
116
  const fileName = `${timeString}_${requestId}.${type}.json`;
89
- const filePath = import_path.default.join(dailyDir, fileName);
90
- import_fs.default.writeFileSync(filePath, JSON.stringify(data, null, 2));
91
- if (type === "request") {
92
- this.createTxtFile(filePath, data);
93
- }
94
- }
95
- createTxtFile(filePath, data) {
96
- const txtContent = this.objectToTxt(data);
97
- const txtFilePath = filePath.replace(".json", ".txt");
98
- import_fs.default.writeFileSync(txtFilePath, txtContent);
99
- }
100
- objectToTxt(obj, prefix = "") {
101
- let result = "";
102
- for (const [key, value] of Object.entries(obj)) {
103
- const newPrefix = prefix ? `${prefix}${key}` : key;
104
- if (typeof value === "object" && value !== null) {
105
- result += `[${newPrefix}]
106
- ${this.objectToTxt(value, `${newPrefix}.`)}
107
- `;
108
- } else {
109
- result += `[${newPrefix}]
110
- ${value}
111
-
112
- `;
117
+ const filePath = import_node_path.default.join(dailyDir, fileName);
118
+ import_node_fs.default.writeFileSync(filePath, JSON.stringify(data, null, 2));
119
+ }
120
+ writeSessionXml(session) {
121
+ try {
122
+ const { dailyDir, timeString } = this.getDailyFolder(session.startTime);
123
+ const xmlFileName = `${timeString}_${session.requestId}.session.xml`;
124
+ const xmlPath = import_node_path.default.join(dailyDir, xmlFileName);
125
+ const xml = this.buildSessionXml(session);
126
+ import_node_fs.default.writeFileSync(xmlPath, xml);
127
+ } catch (error) {
128
+ const errorMessage = error instanceof Error ? error.message : String(error);
129
+ this.logError(`[LLM] Error saving session XML log: ${errorMessage}`, error);
130
+ }
131
+ }
132
+ logError(message, error) {
133
+ if (!this.logger) {
134
+ return;
135
+ }
136
+ const stack = error instanceof Error && error.stack ? `
137
+ ${error.stack}` : "";
138
+ const fullMessage = `${message}${stack}`;
139
+ const logFn = this.logger.error || this.logger.warn || this.logger.log;
140
+ if (logFn) {
141
+ logFn.call(this.logger, fullMessage);
142
+ }
143
+ }
144
+ getDailyFolder(date) {
145
+ const dateString = date.format("YYYY_MM_DD");
146
+ const timeString = date.format("YYYY_MM_DD_HH_mm_ss");
147
+ const dailyDir = import_node_path.default.join(this.options.folder, dateString);
148
+ if (!import_node_fs.default.existsSync(dailyDir)) {
149
+ import_node_fs.default.mkdirSync(dailyDir, { recursive: true });
150
+ }
151
+ return { dailyDir, timeString };
152
+ }
153
+ buildSessionXml(session) {
154
+ const root = (0, import_xmlbuilder2.create)({ version: "1.0", encoding: "UTF-8" }).ele("session", { model: this.sanitizeAttribute(session.model) });
155
+ let toolsUsed = false;
156
+ for (let i = 0; i < session.turns.length; i++) {
157
+ const turn = session.turns[i];
158
+ const turnElem = root.ele("turn", { number: String(i + 1) });
159
+ const requestElem = turnElem.ele("request");
160
+ const messages = Array.isArray(turn.request?.messages) ? turn.request.messages : [];
161
+ for (const message of messages) {
162
+ if (!message || typeof message !== "object") {
163
+ continue;
164
+ }
165
+ const messageElem = requestElem.ele("message", { role: this.sanitizeAttribute(message.role) });
166
+ if (message.role === "tool" && message.tool_call_id !== void 0) {
167
+ messageElem.att("tool_call_id", this.sanitizeAttribute(message.tool_call_id));
168
+ }
169
+ const content = message.content;
170
+ if (typeof content === "string") {
171
+ this.appendCdata(messageElem, content);
172
+ } else if (Array.isArray(content)) {
173
+ const textParts = content.filter((part) => part && typeof part === "object" && part.type === "text").map((part) => part.text ?? "").filter((text) => text.length > 0);
174
+ if (textParts.length > 0) {
175
+ this.appendCdata(messageElem, textParts.join("\n"));
176
+ }
177
+ }
178
+ if (Array.isArray(message.tool_calls) && message.tool_calls.length > 0) {
179
+ toolsUsed = true;
180
+ this.appendToolCalls(messageElem, message.tool_calls);
181
+ }
182
+ }
183
+ if (turn.response) {
184
+ const responseElem = turnElem.ele("response");
185
+ const responseMessageElem = responseElem.ele("message", { role: "assistant" });
186
+ const responseMessage = turn.response?.choices?.[0]?.message;
187
+ const responseContent = responseMessage?.content;
188
+ if (typeof responseContent === "string") {
189
+ this.appendCdata(responseMessageElem, responseContent);
190
+ }
191
+ const responseToolCalls = responseMessage?.tool_calls;
192
+ if (Array.isArray(responseToolCalls) && responseToolCalls.length > 0) {
193
+ toolsUsed = true;
194
+ this.appendToolCalls(responseMessageElem, responseToolCalls);
195
+ }
196
+ }
197
+ if (turn.toolExecutions.length > 0) {
198
+ toolsUsed = true;
199
+ const toolExecsElem = turnElem.ele("tool_executions");
200
+ for (const exec of turn.toolExecutions) {
201
+ const execElem = toolExecsElem.ele("tool_execution", {
202
+ name: this.sanitizeAttribute(exec.toolName),
203
+ counter: String(exec.counter)
204
+ });
205
+ const inputElem = execElem.ele("input");
206
+ if (exec.input && typeof exec.input === "object" && !Array.isArray(exec.input)) {
207
+ for (const [key, value] of Object.entries(exec.input)) {
208
+ const argElem = inputElem.ele("arg", { name: this.sanitizeAttribute(key) });
209
+ this.appendCdata(argElem, value);
210
+ }
211
+ } else {
212
+ this.appendCdata(inputElem, typeof exec.input === "object" ? JSON.stringify(exec.input) : exec.input);
213
+ }
214
+ const outputElem = execElem.ele("output");
215
+ this.appendCdata(outputElem, typeof exec.result === "object" ? JSON.stringify(exec.result) : exec.result);
216
+ }
113
217
  }
114
218
  }
115
- return result;
219
+ if (toolsUsed) {
220
+ root.att("tools_used", "true");
221
+ }
222
+ return root.end({ prettyPrint: true });
223
+ }
224
+ parseArguments(argumentsValue) {
225
+ if (typeof argumentsValue !== "string") {
226
+ return null;
227
+ }
228
+ try {
229
+ const parsed = JSON.parse(argumentsValue);
230
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
231
+ return parsed;
232
+ }
233
+ return null;
234
+ } catch (error) {
235
+ return null;
236
+ }
237
+ }
238
+ appendCdata(target, value) {
239
+ const cleaned = this.cleanXmlString(value);
240
+ if (!cleaned) {
241
+ return;
242
+ }
243
+ const parts = cleaned.split("]]>");
244
+ parts.forEach((part, index) => {
245
+ if (part.length > 0) {
246
+ target.dat(part);
247
+ }
248
+ if (index < parts.length - 1) {
249
+ target.txt("]]>");
250
+ }
251
+ });
252
+ }
253
+ appendArguments(toolElem, args) {
254
+ const argsElem = toolElem.ele("arguments");
255
+ for (const [key, value] of Object.entries(args)) {
256
+ const argElem = argsElem.ele("arg", { name: this.sanitizeAttribute(key) });
257
+ this.appendCdata(argElem, value);
258
+ }
259
+ }
260
+ appendToolCalls(parentElem, toolCalls) {
261
+ const toolCallsElem = parentElem.ele("tool_calls");
262
+ for (const toolCall of toolCalls) {
263
+ if (!toolCall || typeof toolCall !== "object") {
264
+ continue;
265
+ }
266
+ const functionData = toolCall.function ?? {};
267
+ const functionName = functionData.name;
268
+ if (!functionName || functionName === "null") {
269
+ continue;
270
+ }
271
+ const toolElem = toolCallsElem.ele("tool_call");
272
+ if (toolCall.id) {
273
+ toolElem.att("id", this.sanitizeAttribute(toolCall.id));
274
+ }
275
+ toolElem.att("function_name", this.sanitizeAttribute(functionName));
276
+ const args = this.parseArguments(functionData.arguments);
277
+ if (args && typeof args === "object") {
278
+ this.appendArguments(toolElem, args);
279
+ } else if (functionData.arguments !== void 0) {
280
+ this.appendRawArguments(toolElem, functionData.arguments);
281
+ }
282
+ }
283
+ }
284
+ appendRawArguments(toolElem, argumentsValue) {
285
+ const argsElem = toolElem.ele("arguments");
286
+ this.appendCdata(argsElem, argumentsValue);
287
+ }
288
+ sanitizeAttribute(value) {
289
+ return this.cleanXmlString(value);
290
+ }
291
+ cleanXmlString(value) {
292
+ const text = typeof value === "string" ? value : String(value ?? "");
293
+ return [...text].filter((char) => {
294
+ const code = char.codePointAt(0) ?? 0;
295
+ return char === " " || char === "\n" || char === "\r" || code >= 32 && code <= 55295 || code >= 57344 && code <= 65533 || code >= 65536 && code <= 1114111;
296
+ }).join("");
116
297
  }
117
298
  };
118
299
  // Annotate the CommonJS export names for ESM import in node:
package/dist/index.mjs CHANGED
@@ -1,13 +1,15 @@
1
1
  // src/logging.ts
2
- import path from "path";
3
- import fs from "fs";
4
- import { randomBytes } from "crypto";
2
+ import path from "node:path";
3
+ import fs from "node:fs";
4
+ import { randomBytes } from "node:crypto";
5
5
  import moment from "moment";
6
+ import { create } from "xmlbuilder2";
6
7
  function generateRandomDigits(length) {
7
8
  return randomBytes(Math.ceil(length / 2)).toString("hex").slice(0, length);
8
9
  }
9
10
  var LoggingPlugin = class {
10
11
  constructor(options) {
12
+ this.runSessions = /* @__PURE__ */ new Map();
11
13
  this.options = options;
12
14
  this.logger = options.logger;
13
15
  }
@@ -22,60 +24,239 @@ var LoggingPlugin = class {
22
24
  }
23
25
  async wrapRun(next, context) {
24
26
  this.log(`run ${context.model}`);
25
- const response = await next();
26
- const lastResponsePath = path.join(this.options.folder, "last_response.json");
27
- fs.writeFileSync(lastResponsePath, JSON.stringify(response, null, 2));
28
- return response;
27
+ const session = {
28
+ startTime: moment(),
29
+ requestId: generateRandomDigits(4),
30
+ model: context.model,
31
+ turns: []
32
+ };
33
+ this.runSessions.set(context, session);
34
+ try {
35
+ const response = await next();
36
+ const lastResponsePath = path.join(this.options.folder, "last_response.json");
37
+ fs.writeFileSync(lastResponsePath, JSON.stringify(response, null, 2));
38
+ this.writeSessionXml(session);
39
+ return response;
40
+ } catch (error) {
41
+ this.writeSessionXml(session);
42
+ throw error;
43
+ } finally {
44
+ this.runSessions.delete(context);
45
+ }
29
46
  }
30
47
  async wrapExec(next, params, context) {
31
48
  const requestId = generateRandomDigits(4);
32
- this.createLogFiles(requestId, "request", params);
49
+ const requestTime = moment();
50
+ const session = this.runSessions.get(context);
51
+ const turn = { request: params, response: void 0, toolExecutions: [] };
52
+ this.writeJsonLog(requestTime, requestId, "request", params);
53
+ if (session) {
54
+ session.turns.push(turn);
55
+ this.writeSessionXml(session);
56
+ }
33
57
  const response = await next(params);
34
- this.createLogFiles(requestId, "response", response);
58
+ this.writeJsonLog(moment(), requestId, "response", response);
59
+ if (session) {
60
+ turn.response = response;
61
+ this.writeSessionXml(session);
62
+ }
35
63
  return response;
36
64
  }
37
65
  async wrapToolExec(next, tool, counter, input, context) {
38
66
  this.log(`run (${counter + 1}/${context.options.tools?.usesLimit}) tool ${tool.name} ${JSON.stringify(input)}`);
39
67
  const result = await next(tool, counter, input);
40
68
  this.log(`tool response ${JSON.stringify(result)}`);
69
+ const session = this.runSessions.get(context);
70
+ if (session && session.turns.length > 0) {
71
+ const lastTurn = session.turns[session.turns.length - 1];
72
+ lastTurn.toolExecutions.push({ toolName: tool.name, counter, input, result });
73
+ this.writeSessionXml(session);
74
+ }
41
75
  return result;
42
76
  }
43
- createLogFiles(requestId, type, data) {
44
- const currentDate = moment();
45
- const dateString = currentDate.format("YYYY_MM_DD");
46
- const timeString = currentDate.format("YYYY_MM_DD_HH_mm_ss");
77
+ writeJsonLog(date, requestId, type, data) {
78
+ const { dailyDir, timeString } = this.getDailyFolder(date);
79
+ const fileName = `${timeString}_${requestId}.${type}.json`;
80
+ const filePath = path.join(dailyDir, fileName);
81
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
82
+ }
83
+ writeSessionXml(session) {
84
+ try {
85
+ const { dailyDir, timeString } = this.getDailyFolder(session.startTime);
86
+ const xmlFileName = `${timeString}_${session.requestId}.session.xml`;
87
+ const xmlPath = path.join(dailyDir, xmlFileName);
88
+ const xml = this.buildSessionXml(session);
89
+ fs.writeFileSync(xmlPath, xml);
90
+ } catch (error) {
91
+ const errorMessage = error instanceof Error ? error.message : String(error);
92
+ this.logError(`[LLM] Error saving session XML log: ${errorMessage}`, error);
93
+ }
94
+ }
95
+ logError(message, error) {
96
+ if (!this.logger) {
97
+ return;
98
+ }
99
+ const stack = error instanceof Error && error.stack ? `
100
+ ${error.stack}` : "";
101
+ const fullMessage = `${message}${stack}`;
102
+ const logFn = this.logger.error || this.logger.warn || this.logger.log;
103
+ if (logFn) {
104
+ logFn.call(this.logger, fullMessage);
105
+ }
106
+ }
107
+ getDailyFolder(date) {
108
+ const dateString = date.format("YYYY_MM_DD");
109
+ const timeString = date.format("YYYY_MM_DD_HH_mm_ss");
47
110
  const dailyDir = path.join(this.options.folder, dateString);
48
111
  if (!fs.existsSync(dailyDir)) {
49
112
  fs.mkdirSync(dailyDir, { recursive: true });
50
113
  }
51
- const fileName = `${timeString}_${requestId}.${type}.json`;
52
- const filePath = path.join(dailyDir, fileName);
53
- fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
54
- if (type === "request") {
55
- this.createTxtFile(filePath, data);
56
- }
57
- }
58
- createTxtFile(filePath, data) {
59
- const txtContent = this.objectToTxt(data);
60
- const txtFilePath = filePath.replace(".json", ".txt");
61
- fs.writeFileSync(txtFilePath, txtContent);
62
- }
63
- objectToTxt(obj, prefix = "") {
64
- let result = "";
65
- for (const [key, value] of Object.entries(obj)) {
66
- const newPrefix = prefix ? `${prefix}${key}` : key;
67
- if (typeof value === "object" && value !== null) {
68
- result += `[${newPrefix}]
69
- ${this.objectToTxt(value, `${newPrefix}.`)}
70
- `;
71
- } else {
72
- result += `[${newPrefix}]
73
- ${value}
74
-
75
- `;
114
+ return { dailyDir, timeString };
115
+ }
116
+ buildSessionXml(session) {
117
+ const root = create({ version: "1.0", encoding: "UTF-8" }).ele("session", { model: this.sanitizeAttribute(session.model) });
118
+ let toolsUsed = false;
119
+ for (let i = 0; i < session.turns.length; i++) {
120
+ const turn = session.turns[i];
121
+ const turnElem = root.ele("turn", { number: String(i + 1) });
122
+ const requestElem = turnElem.ele("request");
123
+ const messages = Array.isArray(turn.request?.messages) ? turn.request.messages : [];
124
+ for (const message of messages) {
125
+ if (!message || typeof message !== "object") {
126
+ continue;
127
+ }
128
+ const messageElem = requestElem.ele("message", { role: this.sanitizeAttribute(message.role) });
129
+ if (message.role === "tool" && message.tool_call_id !== void 0) {
130
+ messageElem.att("tool_call_id", this.sanitizeAttribute(message.tool_call_id));
131
+ }
132
+ const content = message.content;
133
+ if (typeof content === "string") {
134
+ this.appendCdata(messageElem, content);
135
+ } else if (Array.isArray(content)) {
136
+ const textParts = content.filter((part) => part && typeof part === "object" && part.type === "text").map((part) => part.text ?? "").filter((text) => text.length > 0);
137
+ if (textParts.length > 0) {
138
+ this.appendCdata(messageElem, textParts.join("\n"));
139
+ }
140
+ }
141
+ if (Array.isArray(message.tool_calls) && message.tool_calls.length > 0) {
142
+ toolsUsed = true;
143
+ this.appendToolCalls(messageElem, message.tool_calls);
144
+ }
145
+ }
146
+ if (turn.response) {
147
+ const responseElem = turnElem.ele("response");
148
+ const responseMessageElem = responseElem.ele("message", { role: "assistant" });
149
+ const responseMessage = turn.response?.choices?.[0]?.message;
150
+ const responseContent = responseMessage?.content;
151
+ if (typeof responseContent === "string") {
152
+ this.appendCdata(responseMessageElem, responseContent);
153
+ }
154
+ const responseToolCalls = responseMessage?.tool_calls;
155
+ if (Array.isArray(responseToolCalls) && responseToolCalls.length > 0) {
156
+ toolsUsed = true;
157
+ this.appendToolCalls(responseMessageElem, responseToolCalls);
158
+ }
159
+ }
160
+ if (turn.toolExecutions.length > 0) {
161
+ toolsUsed = true;
162
+ const toolExecsElem = turnElem.ele("tool_executions");
163
+ for (const exec of turn.toolExecutions) {
164
+ const execElem = toolExecsElem.ele("tool_execution", {
165
+ name: this.sanitizeAttribute(exec.toolName),
166
+ counter: String(exec.counter)
167
+ });
168
+ const inputElem = execElem.ele("input");
169
+ if (exec.input && typeof exec.input === "object" && !Array.isArray(exec.input)) {
170
+ for (const [key, value] of Object.entries(exec.input)) {
171
+ const argElem = inputElem.ele("arg", { name: this.sanitizeAttribute(key) });
172
+ this.appendCdata(argElem, value);
173
+ }
174
+ } else {
175
+ this.appendCdata(inputElem, typeof exec.input === "object" ? JSON.stringify(exec.input) : exec.input);
176
+ }
177
+ const outputElem = execElem.ele("output");
178
+ this.appendCdata(outputElem, typeof exec.result === "object" ? JSON.stringify(exec.result) : exec.result);
179
+ }
76
180
  }
77
181
  }
78
- return result;
182
+ if (toolsUsed) {
183
+ root.att("tools_used", "true");
184
+ }
185
+ return root.end({ prettyPrint: true });
186
+ }
187
+ parseArguments(argumentsValue) {
188
+ if (typeof argumentsValue !== "string") {
189
+ return null;
190
+ }
191
+ try {
192
+ const parsed = JSON.parse(argumentsValue);
193
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
194
+ return parsed;
195
+ }
196
+ return null;
197
+ } catch (error) {
198
+ return null;
199
+ }
200
+ }
201
+ appendCdata(target, value) {
202
+ const cleaned = this.cleanXmlString(value);
203
+ if (!cleaned) {
204
+ return;
205
+ }
206
+ const parts = cleaned.split("]]>");
207
+ parts.forEach((part, index) => {
208
+ if (part.length > 0) {
209
+ target.dat(part);
210
+ }
211
+ if (index < parts.length - 1) {
212
+ target.txt("]]>");
213
+ }
214
+ });
215
+ }
216
+ appendArguments(toolElem, args) {
217
+ const argsElem = toolElem.ele("arguments");
218
+ for (const [key, value] of Object.entries(args)) {
219
+ const argElem = argsElem.ele("arg", { name: this.sanitizeAttribute(key) });
220
+ this.appendCdata(argElem, value);
221
+ }
222
+ }
223
+ appendToolCalls(parentElem, toolCalls) {
224
+ const toolCallsElem = parentElem.ele("tool_calls");
225
+ for (const toolCall of toolCalls) {
226
+ if (!toolCall || typeof toolCall !== "object") {
227
+ continue;
228
+ }
229
+ const functionData = toolCall.function ?? {};
230
+ const functionName = functionData.name;
231
+ if (!functionName || functionName === "null") {
232
+ continue;
233
+ }
234
+ const toolElem = toolCallsElem.ele("tool_call");
235
+ if (toolCall.id) {
236
+ toolElem.att("id", this.sanitizeAttribute(toolCall.id));
237
+ }
238
+ toolElem.att("function_name", this.sanitizeAttribute(functionName));
239
+ const args = this.parseArguments(functionData.arguments);
240
+ if (args && typeof args === "object") {
241
+ this.appendArguments(toolElem, args);
242
+ } else if (functionData.arguments !== void 0) {
243
+ this.appendRawArguments(toolElem, functionData.arguments);
244
+ }
245
+ }
246
+ }
247
+ appendRawArguments(toolElem, argumentsValue) {
248
+ const argsElem = toolElem.ele("arguments");
249
+ this.appendCdata(argsElem, argumentsValue);
250
+ }
251
+ sanitizeAttribute(value) {
252
+ return this.cleanXmlString(value);
253
+ }
254
+ cleanXmlString(value) {
255
+ const text = typeof value === "string" ? value : String(value ?? "");
256
+ return [...text].filter((char) => {
257
+ const code = char.codePointAt(0) ?? 0;
258
+ return char === " " || char === "\n" || char === "\r" || code >= 32 && code <= 55295 || code >= 57344 && code <= 65533 || code >= 65536 && code <= 1114111;
259
+ }).join("");
79
260
  }
80
261
  };
81
262
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llmbridge/plugin-logging",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Logging plugin for LLMBridge",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -24,7 +24,8 @@
24
24
  "access": "public"
25
25
  },
26
26
  "dependencies": {
27
- "moment": "^2.30.1"
27
+ "moment": "^2.30.1",
28
+ "xmlbuilder2": "^3.1.1"
28
29
  },
29
30
  "peerDependencies": {
30
31
  "@llmbridge/core": ">=0.1.0 <0.2.0",