goblin-agents 2.1.1 → 2.2.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/lib/llm/aiAgent.js +32 -3
- package/lib/llm/providers/ollama.js +97 -0
- package/lib/llm/providers/opanAi.js +101 -0
- package/lib/llm/providers/provider.js +36 -0
- package/lib/llm/providers.js +5 -214
- package/lib/logger.js +104 -0
- package/lib/report/item.html +2 -0
- package/lib/report/message.html +8 -0
- package/lib/report/report.html +10 -0
- package/package.json +1 -1
package/lib/llm/aiAgent.js
CHANGED
|
@@ -12,8 +12,9 @@ const {
|
|
|
12
12
|
record,
|
|
13
13
|
} = require('xcraft-core-stones');
|
|
14
14
|
const {getMutex} = require('xcraft-core-utils/lib/locks.js');
|
|
15
|
-
const
|
|
15
|
+
const LLMProviders = require('./providers.js');
|
|
16
16
|
const {OCR_SYSTEM_PROMPT, getPDFImages, splitThinkTags} = require('./utils.js');
|
|
17
|
+
const Logger = require('../logger.js');
|
|
17
18
|
|
|
18
19
|
class MessageShape {
|
|
19
20
|
role = enumeration('assistant', 'user', 'tool');
|
|
@@ -352,7 +353,7 @@ class AiAgent extends Elf {
|
|
|
352
353
|
return;
|
|
353
354
|
}
|
|
354
355
|
|
|
355
|
-
const Instance =
|
|
356
|
+
const Instance = LLMProviders.get(this.state.provider);
|
|
356
357
|
this._provider = new Instance(this.state.host, this.state.headers.toJS());
|
|
357
358
|
this._conf = {provider, host, headersStr};
|
|
358
359
|
}
|
|
@@ -378,7 +379,9 @@ class AiAgent extends Elf {
|
|
|
378
379
|
await this.persist();
|
|
379
380
|
}
|
|
380
381
|
|
|
381
|
-
async gen(prompt) {
|
|
382
|
+
async gen(prompt, contextId = 'default') {
|
|
383
|
+
const logger = Logger.get(contextId, this);
|
|
384
|
+
|
|
382
385
|
await this._mutex.lock(this.id);
|
|
383
386
|
this.quest.defer(() => this._mutex.unlock(this.id));
|
|
384
387
|
|
|
@@ -402,6 +405,18 @@ class AiAgent extends Elf {
|
|
|
402
405
|
options,
|
|
403
406
|
stream: false,
|
|
404
407
|
});
|
|
408
|
+
|
|
409
|
+
logger.push('aiAgent.gen', {
|
|
410
|
+
id: this.id,
|
|
411
|
+
model: this.state.model,
|
|
412
|
+
options: Object.entries(options).reduce(
|
|
413
|
+
(out, opt) => `${out} ${opt[0]}=${opt[1]}`,
|
|
414
|
+
''
|
|
415
|
+
),
|
|
416
|
+
reasoning: this.state.reasoning,
|
|
417
|
+
format: format || 'N/A',
|
|
418
|
+
});
|
|
419
|
+
|
|
405
420
|
return cleanText(res);
|
|
406
421
|
}
|
|
407
422
|
|
|
@@ -499,6 +514,8 @@ class AiAgent extends Elf {
|
|
|
499
514
|
}
|
|
500
515
|
|
|
501
516
|
async chat(contextId, question, userDesktopId) {
|
|
517
|
+
const logger = Logger.get(contextId, this);
|
|
518
|
+
|
|
502
519
|
await this._mutex.lock(this.id);
|
|
503
520
|
this.quest.defer(() => this._mutex.unlock(this.id));
|
|
504
521
|
|
|
@@ -534,6 +551,18 @@ class AiAgent extends Elf {
|
|
|
534
551
|
|
|
535
552
|
const result = await this._provider.chat(chatCompletionRequest);
|
|
536
553
|
|
|
554
|
+
logger.push('aiAgent.chat', {
|
|
555
|
+
id: this.id,
|
|
556
|
+
model: chatCompletionRequest.model,
|
|
557
|
+
options: Object.entries(chatCompletionRequest.options).reduce(
|
|
558
|
+
(out, opt) => `${out} ${opt[0]}=${opt[1]}`,
|
|
559
|
+
''
|
|
560
|
+
),
|
|
561
|
+
question,
|
|
562
|
+
reasoning: result.message?.reasoning,
|
|
563
|
+
callTools: !!result.message.tool_calls?.length,
|
|
564
|
+
});
|
|
565
|
+
|
|
537
566
|
if (this.state.toolServiceId && this.state.tools) {
|
|
538
567
|
const used = await this.callTools(
|
|
539
568
|
contextId,
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
const {Ollama} = require('ollama');
|
|
3
|
+
const {Semaphore} = require('xcraft-core-utils/lib/locks.js');
|
|
4
|
+
const LLMProvider = require('./provider.js');
|
|
5
|
+
|
|
6
|
+
class OllamaProvider extends LLMProvider {
|
|
7
|
+
#semEmbed = new Semaphore(4);
|
|
8
|
+
#ollama;
|
|
9
|
+
|
|
10
|
+
constructor(host, headers) {
|
|
11
|
+
super();
|
|
12
|
+
this.#ollama = new Ollama({
|
|
13
|
+
host,
|
|
14
|
+
headers,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async chat(chatRequest) {
|
|
19
|
+
const {
|
|
20
|
+
model,
|
|
21
|
+
messages,
|
|
22
|
+
options,
|
|
23
|
+
format,
|
|
24
|
+
tools,
|
|
25
|
+
stream,
|
|
26
|
+
reasoning,
|
|
27
|
+
} = chatRequest;
|
|
28
|
+
const useFormat = !!format;
|
|
29
|
+
const chat = await this.#ollama.chat({
|
|
30
|
+
model,
|
|
31
|
+
messages,
|
|
32
|
+
options,
|
|
33
|
+
format,
|
|
34
|
+
tools,
|
|
35
|
+
stream,
|
|
36
|
+
think: reasoning,
|
|
37
|
+
});
|
|
38
|
+
if (useFormat) {
|
|
39
|
+
chat.message.content = JSON.parse(chat.message.content);
|
|
40
|
+
return chat;
|
|
41
|
+
} else {
|
|
42
|
+
return chat;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async gen(genRequest) {
|
|
47
|
+
const {
|
|
48
|
+
model,
|
|
49
|
+
system,
|
|
50
|
+
prompt,
|
|
51
|
+
format,
|
|
52
|
+
options,
|
|
53
|
+
stream,
|
|
54
|
+
reasoning,
|
|
55
|
+
} = genRequest;
|
|
56
|
+
const useFormat = !!format;
|
|
57
|
+
const gen = await this.#ollama.generate({
|
|
58
|
+
model,
|
|
59
|
+
prompt,
|
|
60
|
+
system,
|
|
61
|
+
format,
|
|
62
|
+
options,
|
|
63
|
+
stream,
|
|
64
|
+
think: reasoning,
|
|
65
|
+
});
|
|
66
|
+
if (useFormat) {
|
|
67
|
+
return JSON.parse(gen.response);
|
|
68
|
+
} else {
|
|
69
|
+
return gen.response;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async embed(embedRequest) {
|
|
74
|
+
try {
|
|
75
|
+
await this.#semEmbed.wait();
|
|
76
|
+
const {model, input} = embedRequest;
|
|
77
|
+
const {embeddings} = await this.#ollama.embed({
|
|
78
|
+
model,
|
|
79
|
+
input,
|
|
80
|
+
});
|
|
81
|
+
return embeddings[0];
|
|
82
|
+
} finally {
|
|
83
|
+
this.#semEmbed.signal();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async embedInBatch(embedRequest) {
|
|
88
|
+
const {model, input} = embedRequest;
|
|
89
|
+
const {embeddings} = await this.#ollama.embed({
|
|
90
|
+
model,
|
|
91
|
+
input,
|
|
92
|
+
});
|
|
93
|
+
return embeddings;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = OllamaProvider;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
const {RestAPI} = require('xcraft-core-utils');
|
|
3
|
+
const LLMProvider = require('./provider.js');
|
|
4
|
+
|
|
5
|
+
class OpenAIProvider extends LLMProvider {
|
|
6
|
+
#baseUrl;
|
|
7
|
+
#openAI;
|
|
8
|
+
|
|
9
|
+
constructor(host = 'https://openrouter.ai/api/v1', headers) {
|
|
10
|
+
super();
|
|
11
|
+
this.#baseUrl = host;
|
|
12
|
+
this.#openAI = new RestAPI(300000, headers);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async chat(chatRequest) {
|
|
16
|
+
delete chatRequest.reasoning; //not yet supported
|
|
17
|
+
if (chatRequest.options) {
|
|
18
|
+
chatRequest = {...chatRequest, ...chatRequest.options};
|
|
19
|
+
delete chatRequest.options;
|
|
20
|
+
}
|
|
21
|
+
if (!chatRequest.tools) {
|
|
22
|
+
delete chatRequest.tools;
|
|
23
|
+
}
|
|
24
|
+
if (!chatRequest.format) {
|
|
25
|
+
delete chatRequest.format;
|
|
26
|
+
}
|
|
27
|
+
const useFormat = chatRequest?.format !== undefined;
|
|
28
|
+
if (useFormat) {
|
|
29
|
+
chatRequest.response_format = {
|
|
30
|
+
type: 'json_schema',
|
|
31
|
+
json_schema: {
|
|
32
|
+
name: 'default',
|
|
33
|
+
schema: {
|
|
34
|
+
type: 'object',
|
|
35
|
+
properties: {
|
|
36
|
+
results: {...chatRequest.format},
|
|
37
|
+
},
|
|
38
|
+
required: ['results'],
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
delete chatRequest.format;
|
|
44
|
+
}
|
|
45
|
+
const query = `${this.#baseUrl}/chat/completions`;
|
|
46
|
+
const chat = await this.#openAI._post(query, chatRequest, {
|
|
47
|
+
retry: {
|
|
48
|
+
methods: ['POST'],
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
const {choices} = chat;
|
|
52
|
+
if (chat.error) {
|
|
53
|
+
const err = new Error(
|
|
54
|
+
chat.error?.metadata
|
|
55
|
+
? JSON.stringify(chat.error?.metadata)
|
|
56
|
+
: chat.error.message
|
|
57
|
+
);
|
|
58
|
+
err.code = chat.error.code;
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
61
|
+
if (useFormat) {
|
|
62
|
+
choices[0].message.content = JSON.parse(
|
|
63
|
+
choices[0].message.content
|
|
64
|
+
).results;
|
|
65
|
+
return choices[0];
|
|
66
|
+
} else {
|
|
67
|
+
return choices[0];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async gen(genRequest) {
|
|
72
|
+
const {model, system, prompt, format, tools, options, stream} = genRequest;
|
|
73
|
+
const chatRequest = {
|
|
74
|
+
model,
|
|
75
|
+
messages: [
|
|
76
|
+
{role: 'system', content: system},
|
|
77
|
+
{role: 'user', content: prompt},
|
|
78
|
+
],
|
|
79
|
+
options,
|
|
80
|
+
format,
|
|
81
|
+
tools,
|
|
82
|
+
stream,
|
|
83
|
+
};
|
|
84
|
+
const chat = await this.chat(chatRequest);
|
|
85
|
+
return chat.message.content;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async embed(embedRequest) {
|
|
89
|
+
const query = `${this.#baseUrl}/v1/embeddings`;
|
|
90
|
+
const {data} = await this.#openAI._post(query, embedRequest);
|
|
91
|
+
return data[0].embedding;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async embedInBatch(embedRequest) {
|
|
95
|
+
const query = `${this.#baseUrl}/v1/embeddings`;
|
|
96
|
+
const {data} = await this.#openAI._post(query, embedRequest);
|
|
97
|
+
return data;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = OpenAIProvider;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
class LLMProvider {
|
|
4
|
+
constructor() {
|
|
5
|
+
if (new.target === LLMProvider) {
|
|
6
|
+
throw new Error('Cannot instantiate an abstract class.');
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async chatStream() {
|
|
11
|
+
throw new Error('not impl.');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @returns {Promise<*>}
|
|
16
|
+
*/
|
|
17
|
+
async chat() {
|
|
18
|
+
throw new Error('not impl.');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @returns {Promise<*>}
|
|
23
|
+
*/
|
|
24
|
+
async gen() {
|
|
25
|
+
throw new Error('not impl.');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @returns {Promise<*>}
|
|
30
|
+
*/
|
|
31
|
+
async embed() {
|
|
32
|
+
throw new Error('not impl.');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = LLMProvider;
|
package/lib/llm/providers.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
const
|
|
3
|
-
const
|
|
1
|
+
// @ts-check
|
|
2
|
+
const OllamaProvider = require('./providers/ollama.js');
|
|
3
|
+
const OpenAIProvider = require('./providers/opanAi.js');
|
|
4
4
|
|
|
5
|
-
class
|
|
5
|
+
class LLMProviders {
|
|
6
6
|
static get(name) {
|
|
7
7
|
switch (name) {
|
|
8
8
|
default:
|
|
@@ -12,215 +12,6 @@ class LLMProvider {
|
|
|
12
12
|
return OpenAIProvider;
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
-
|
|
16
|
-
constructor() {
|
|
17
|
-
if (new.target === LLMProvider) {
|
|
18
|
-
throw new Error('Cannot instantiate an abstract class.');
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async chatStream() {
|
|
23
|
-
throw new Error('not impl.');
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async chat() {
|
|
27
|
-
throw new Error('not impl.');
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async gen() {
|
|
31
|
-
throw new Error('not impl.');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async embed() {
|
|
35
|
-
throw new Error('not impl.');
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
class OllamaProvider extends LLMProvider {
|
|
40
|
-
#semEmbed = new Semaphore(4);
|
|
41
|
-
#ollama;
|
|
42
|
-
|
|
43
|
-
constructor(host, headers) {
|
|
44
|
-
super();
|
|
45
|
-
this.#ollama = new Ollama({
|
|
46
|
-
host,
|
|
47
|
-
headers,
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async chat(chatRequest) {
|
|
52
|
-
const {
|
|
53
|
-
model,
|
|
54
|
-
messages,
|
|
55
|
-
options,
|
|
56
|
-
format,
|
|
57
|
-
tools,
|
|
58
|
-
stream,
|
|
59
|
-
reasoning,
|
|
60
|
-
} = chatRequest;
|
|
61
|
-
const useFormat = !!format;
|
|
62
|
-
const chat = await this.#ollama.chat({
|
|
63
|
-
model,
|
|
64
|
-
messages,
|
|
65
|
-
options,
|
|
66
|
-
format,
|
|
67
|
-
tools,
|
|
68
|
-
stream,
|
|
69
|
-
think: reasoning,
|
|
70
|
-
});
|
|
71
|
-
if (useFormat) {
|
|
72
|
-
chat.message.content = JSON.parse(chat.message.content);
|
|
73
|
-
return chat;
|
|
74
|
-
} else {
|
|
75
|
-
return chat;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async gen(genRequest) {
|
|
80
|
-
const {
|
|
81
|
-
model,
|
|
82
|
-
system,
|
|
83
|
-
prompt,
|
|
84
|
-
format,
|
|
85
|
-
options,
|
|
86
|
-
stream,
|
|
87
|
-
reasoning,
|
|
88
|
-
} = genRequest;
|
|
89
|
-
const useFormat = !!format;
|
|
90
|
-
const gen = await this.#ollama.generate({
|
|
91
|
-
model,
|
|
92
|
-
prompt,
|
|
93
|
-
system,
|
|
94
|
-
format,
|
|
95
|
-
options,
|
|
96
|
-
stream,
|
|
97
|
-
think: reasoning,
|
|
98
|
-
});
|
|
99
|
-
if (useFormat) {
|
|
100
|
-
return JSON.parse(gen.response);
|
|
101
|
-
} else {
|
|
102
|
-
return gen.response;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
async embed(embedRequest) {
|
|
107
|
-
try {
|
|
108
|
-
await this.#semEmbed.wait();
|
|
109
|
-
const {model, input} = embedRequest;
|
|
110
|
-
const {embeddings} = await this.#ollama.embed({
|
|
111
|
-
model,
|
|
112
|
-
input,
|
|
113
|
-
});
|
|
114
|
-
return embeddings[0];
|
|
115
|
-
} finally {
|
|
116
|
-
this.#semEmbed.signal();
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
async embedInBatch(embedRequest) {
|
|
121
|
-
const {model, input} = embedRequest;
|
|
122
|
-
const {embeddings} = await this.#ollama.embed({
|
|
123
|
-
model,
|
|
124
|
-
input,
|
|
125
|
-
});
|
|
126
|
-
return embeddings;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
class OpenAIProvider extends LLMProvider {
|
|
131
|
-
#baseUrl;
|
|
132
|
-
#openAI;
|
|
133
|
-
|
|
134
|
-
constructor(host = 'https://openrouter.ai/api/v1', headers) {
|
|
135
|
-
super();
|
|
136
|
-
this.#baseUrl = host;
|
|
137
|
-
this.#openAI = new RestAPI(300000, headers);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async chat(chatRequest) {
|
|
141
|
-
delete chatRequest.reasoning; //not yet supported
|
|
142
|
-
if (chatRequest.options) {
|
|
143
|
-
chatRequest = {...chatRequest, ...chatRequest.options};
|
|
144
|
-
delete chatRequest.options;
|
|
145
|
-
}
|
|
146
|
-
if (!chatRequest.tools) {
|
|
147
|
-
delete chatRequest.tools;
|
|
148
|
-
}
|
|
149
|
-
if (!chatRequest.format) {
|
|
150
|
-
delete chatRequest.format;
|
|
151
|
-
}
|
|
152
|
-
const useFormat = chatRequest?.format !== undefined;
|
|
153
|
-
if (useFormat) {
|
|
154
|
-
chatRequest.response_format = {
|
|
155
|
-
type: 'json_schema',
|
|
156
|
-
json_schema: {
|
|
157
|
-
name: 'default',
|
|
158
|
-
schema: {
|
|
159
|
-
type: 'object',
|
|
160
|
-
properties: {
|
|
161
|
-
results: {...chatRequest.format},
|
|
162
|
-
},
|
|
163
|
-
required: ['results'],
|
|
164
|
-
},
|
|
165
|
-
},
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
delete chatRequest.format;
|
|
169
|
-
}
|
|
170
|
-
const query = `${this.#baseUrl}/chat/completions`;
|
|
171
|
-
const chat = await this.#openAI._post(query, chatRequest, {
|
|
172
|
-
retry: {
|
|
173
|
-
methods: ['POST'],
|
|
174
|
-
},
|
|
175
|
-
});
|
|
176
|
-
const {choices} = chat;
|
|
177
|
-
if (chat.error) {
|
|
178
|
-
const err = new Error(
|
|
179
|
-
chat.error?.metadata
|
|
180
|
-
? JSON.stringify(chat.error?.metadata)
|
|
181
|
-
: chat.error.message
|
|
182
|
-
);
|
|
183
|
-
err.code = chat.error.code;
|
|
184
|
-
throw err;
|
|
185
|
-
}
|
|
186
|
-
if (useFormat) {
|
|
187
|
-
choices[0].message.content = JSON.parse(
|
|
188
|
-
choices[0].message.content
|
|
189
|
-
).results;
|
|
190
|
-
return choices[0];
|
|
191
|
-
} else {
|
|
192
|
-
return choices[0];
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
async gen(genRequest) {
|
|
197
|
-
const {model, system, prompt, format, tools, options, stream} = genRequest;
|
|
198
|
-
const chatRequest = {
|
|
199
|
-
model,
|
|
200
|
-
messages: [
|
|
201
|
-
{role: 'system', content: system},
|
|
202
|
-
{role: 'user', content: prompt},
|
|
203
|
-
],
|
|
204
|
-
options,
|
|
205
|
-
format,
|
|
206
|
-
tools,
|
|
207
|
-
stream,
|
|
208
|
-
};
|
|
209
|
-
const chat = await this.chat(chatRequest);
|
|
210
|
-
return chat.message.content;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
async embed(embedRequest) {
|
|
214
|
-
const query = `${this.#baseUrl}/v1/embeddings`;
|
|
215
|
-
const {data} = await this.#openAI._post(query, embedRequest);
|
|
216
|
-
return data[0].embedding;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
async embedInBatch(embedRequest) {
|
|
220
|
-
const query = `${this.#baseUrl}/v1/embeddings`;
|
|
221
|
-
const {data} = await this.#openAI._post(query, embedRequest);
|
|
222
|
-
return data;
|
|
223
|
-
}
|
|
224
15
|
}
|
|
225
16
|
|
|
226
|
-
module.exports =
|
|
17
|
+
module.exports = LLMProviders;
|
package/lib/logger.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const fse = require('fs-extra');
|
|
4
|
+
const {string, Sculpt, dateTime, object} = require('xcraft-core-stones');
|
|
5
|
+
const xConfig = require('xcraft-core-etc')().load('xcraft');
|
|
6
|
+
|
|
7
|
+
class MessageShape {
|
|
8
|
+
timestamp = dateTime;
|
|
9
|
+
step = string;
|
|
10
|
+
payload = object;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
class Message extends Sculpt(MessageShape) {}
|
|
14
|
+
|
|
15
|
+
class Logger {
|
|
16
|
+
#id;
|
|
17
|
+
|
|
18
|
+
/** @type {Set<Message>} */
|
|
19
|
+
#messages = new Set();
|
|
20
|
+
|
|
21
|
+
/** @type {Map<string, Logger>} */
|
|
22
|
+
static #loggers = new Map();
|
|
23
|
+
static #logDir = path.join(xConfig.xcraftRoot, 'var/log/ai');
|
|
24
|
+
|
|
25
|
+
constructor(id) {
|
|
26
|
+
this.#id = id;
|
|
27
|
+
fse.ensureDirSync(Logger.#logDir);
|
|
28
|
+
Logger.#loggers.set(id, this);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {string} step
|
|
33
|
+
* @param {object} payload
|
|
34
|
+
*/
|
|
35
|
+
push(step, payload) {
|
|
36
|
+
const timestamp = new Date().toISOString();
|
|
37
|
+
const msg = new Message({timestamp, step, payload});
|
|
38
|
+
this.#messages.add(msg);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async #report() {
|
|
42
|
+
let timestamp = new Date().toISOString();
|
|
43
|
+
let messages = '';
|
|
44
|
+
|
|
45
|
+
for (const message of this.#messages) {
|
|
46
|
+
timestamp = message.timestamp;
|
|
47
|
+
|
|
48
|
+
const messageFile = path.join(__dirname, 'report/message.html');
|
|
49
|
+
let reportMessage = await fse.readFile(messageFile, 'utf8');
|
|
50
|
+
|
|
51
|
+
let payload = '';
|
|
52
|
+
for (const [key, value] of Object.entries(message.payload)) {
|
|
53
|
+
const itemFile = path.join(__dirname, 'report/item.html');
|
|
54
|
+
let reportItem = await fse.readFile(itemFile, 'utf8');
|
|
55
|
+
payload += reportItem
|
|
56
|
+
.replace('${KEY}', key)
|
|
57
|
+
.replace('${VALUE}', `${value}`.replaceAll('\n', '<br/>'));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
messages += reportMessage
|
|
61
|
+
.replace('${TIMESTAMP}', message.timestamp)
|
|
62
|
+
.replace('${STEP}', message.step)
|
|
63
|
+
.replace('${PAYLOAD}', payload);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const reportFile = path.join(__dirname, 'report/report.html');
|
|
67
|
+
let report = await fse.readFile(reportFile, 'utf8');
|
|
68
|
+
report = report
|
|
69
|
+
.replace('${MESSAGES}', messages)
|
|
70
|
+
.replace('${TITLE}', `AI reporting for ${this.#id}`);
|
|
71
|
+
|
|
72
|
+
const ts = timestamp.split(':').slice(0, 2).join('').split('-').join('');
|
|
73
|
+
const reportOutput = path.join(Logger.#logDir, `${this.#id}_${ts}Z.html`);
|
|
74
|
+
await fse.writeFile(reportOutput, report, 'utf8');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async dispose() {
|
|
78
|
+
if (this.#id && this.#id !== 'default') {
|
|
79
|
+
await this.#report();
|
|
80
|
+
}
|
|
81
|
+
Logger.#loggers.delete(this.#id);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
static get(id, quest) {
|
|
85
|
+
if (quest.quest) {
|
|
86
|
+
quest = quest.quest;
|
|
87
|
+
}
|
|
88
|
+
if (Logger.#loggers.has(id)) {
|
|
89
|
+
const logger = Logger.#loggers.get(id);
|
|
90
|
+
if (logger) {
|
|
91
|
+
return logger;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const logger = new Logger(id);
|
|
95
|
+
quest.defer(async () => await logger.dispose());
|
|
96
|
+
return logger;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
static get logDir() {
|
|
100
|
+
return Logger.#logDir;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = Logger;
|