newo 3.6.1 ā 3.7.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.md +41 -0
- package/README.md +61 -0
- package/dist/cli/commands/check.d.ts +3 -0
- package/dist/cli/commands/check.js +15 -0
- package/dist/cli/commands/format.d.ts +3 -0
- package/dist/cli/commands/format.js +105 -0
- package/dist/cli/commands/help.js +13 -0
- package/dist/cli/commands/lint.d.ts +3 -0
- package/dist/cli/commands/lint.js +195 -0
- package/dist/cli-new/di/tokens.d.ts +1 -1
- package/dist/cli.js +45 -9
- package/dist/domain/strategies/sync/AttributeSyncStrategy.js +7 -4
- package/dist/lint/config.d.ts +4 -0
- package/dist/lint/config.js +14 -0
- package/dist/lint/discovery.d.ts +34 -0
- package/dist/lint/discovery.js +112 -0
- package/dist/lint/live-schema.d.ts +20 -0
- package/dist/lint/live-schema.js +52 -0
- package/dist/lint/reporters/index.d.ts +4 -0
- package/dist/lint/reporters/index.js +19 -0
- package/dist/lint/reporters/json.d.ts +3 -0
- package/dist/lint/reporters/json.js +6 -0
- package/dist/lint/reporters/sarif.d.ts +3 -0
- package/dist/lint/reporters/sarif.js +47 -0
- package/dist/lint/reporters/text.d.ts +3 -0
- package/dist/lint/reporters/text.js +51 -0
- package/dist/lint/reporters/types.d.ts +6 -0
- package/dist/lint/reporters/types.js +2 -0
- package/dist/sync/attributes.js +14 -14
- package/dist/sync/conversations.d.ts +1 -1
- package/dist/sync/conversations.js +240 -193
- package/package.json +3 -1
- package/src/cli/commands/check.ts +21 -0
- package/src/cli/commands/format.ts +131 -0
- package/src/cli/commands/help.ts +13 -0
- package/src/cli/commands/lint.ts +246 -0
- package/src/cli.ts +50 -9
- package/src/domain/strategies/sync/AttributeSyncStrategy.ts +7 -4
- package/src/lint/config.ts +17 -0
- package/src/lint/discovery.ts +148 -0
- package/src/lint/live-schema.ts +62 -0
- package/src/lint/reporters/index.ts +22 -0
- package/src/lint/reporters/json.ts +12 -0
- package/src/lint/reporters/sarif.ts +59 -0
- package/src/lint/reporters/text.ts +58 -0
- package/src/lint/reporters/types.ts +7 -0
- package/src/sync/attributes.ts +14 -14
- package/src/sync/conversations.ts +265 -212
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human-readable terminal reporter. Mirrors the ESLint 'stylish' layout:
|
|
3
|
+
*
|
|
4
|
+
* path/to/file.jinja
|
|
5
|
+
* 12:5 error Unknown skill: foo. Did you mean: bar? E100
|
|
6
|
+
* ...
|
|
7
|
+
*
|
|
8
|
+
* 2 problems (1 error, 1 warning)
|
|
9
|
+
*/
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import type { ProjectLintReport, LintResult } from 'newo-dsl-analyzer';
|
|
12
|
+
import type { Reporter } from './types.js';
|
|
13
|
+
|
|
14
|
+
const RED = '\x1b[31m';
|
|
15
|
+
const YELLOW = '\x1b[33m';
|
|
16
|
+
const CYAN = '\x1b[36m';
|
|
17
|
+
const GREY = '\x1b[90m';
|
|
18
|
+
const RESET = '\x1b[0m';
|
|
19
|
+
const BOLD = '\x1b[1m';
|
|
20
|
+
|
|
21
|
+
export const textReporter: Reporter = {
|
|
22
|
+
write(report: ProjectLintReport): string {
|
|
23
|
+
const lines: string[] = [];
|
|
24
|
+
const filesWithIssues = report.results.filter(r => r.diagnostics.length > 0);
|
|
25
|
+
|
|
26
|
+
for (const result of filesWithIssues) {
|
|
27
|
+
lines.push(renderFile(result));
|
|
28
|
+
lines.push('');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
lines.push(renderSummary(report));
|
|
32
|
+
return lines.join('\n');
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function renderFile(result: LintResult): string {
|
|
37
|
+
const rel = path.relative(process.cwd(), result.filePath);
|
|
38
|
+
const header = `${BOLD}${CYAN}${rel}${RESET}`;
|
|
39
|
+
const rows = result.diagnostics.map(d => {
|
|
40
|
+
const loc = `${d.range.start.line}:${d.range.start.column}`;
|
|
41
|
+
const sev = d.severity === 'error'
|
|
42
|
+
? `${RED}error${RESET}`
|
|
43
|
+
: d.severity === 'warning'
|
|
44
|
+
? `${YELLOW}warning${RESET}`
|
|
45
|
+
: `${GREY}${d.severity}${RESET}`;
|
|
46
|
+
return ` ${loc.padEnd(7)} ${sev.padEnd(16)} ${d.message} ${GREY}${d.code}${RESET}`;
|
|
47
|
+
});
|
|
48
|
+
return [header, ...rows].join('\n');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function renderSummary(report: ProjectLintReport): string {
|
|
52
|
+
const total = report.errorCount + report.warningCount;
|
|
53
|
+
if (total === 0) {
|
|
54
|
+
return `${GREY}No issues found.${RESET}`;
|
|
55
|
+
}
|
|
56
|
+
const color = report.errorCount > 0 ? RED : YELLOW;
|
|
57
|
+
return `${color}${BOLD}${total} problems${RESET} (${report.errorCount} error${report.errorCount === 1 ? '' : 's'}, ${report.warningCount} warning${report.warningCount === 1 ? '' : 's'})`;
|
|
58
|
+
}
|
package/src/sync/attributes.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import path from 'path';
|
|
12
12
|
import fs from 'fs-extra';
|
|
13
13
|
import yaml from 'js-yaml';
|
|
14
|
+
import { patchYamlToPyyaml } from '../format/yaml-patch.js';
|
|
14
15
|
import type { AxiosInstance } from 'axios';
|
|
15
16
|
import type { CustomerConfig } from '../types.js';
|
|
16
17
|
|
|
@@ -72,25 +73,24 @@ export async function saveCustomerAttributes(
|
|
|
72
73
|
attributes: cleanAttributes
|
|
73
74
|
};
|
|
74
75
|
|
|
75
|
-
//
|
|
76
|
+
// Emit YAML without folding/wrapping; patchYamlToPyyaml handles long-line
|
|
77
|
+
// wrapping and converts JSON-like double-quoted values to single-quoted
|
|
78
|
+
// (so strings containing `"` stay valid YAML on reload).
|
|
76
79
|
let yamlContent = yaml.dump(attributesYaml, {
|
|
77
80
|
indent: 2,
|
|
78
81
|
quotingType: '"',
|
|
79
82
|
forceQuotes: false,
|
|
80
|
-
lineWidth:
|
|
83
|
+
lineWidth: -1,
|
|
81
84
|
noRefs: true,
|
|
82
85
|
sortKeys: false,
|
|
83
|
-
flowLevel: -1,
|
|
84
|
-
styles: {
|
|
85
|
-
'!!str': 'folded' // Use folded style for better line wrapping of long strings
|
|
86
|
-
}
|
|
86
|
+
flowLevel: -1,
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
-
// Post-process to fix enum format
|
|
89
|
+
// Post-process to fix enum format
|
|
90
90
|
yamlContent = yamlContent.replace(/__ENUM_PLACEHOLDER_(\w+)__/g, '!enum "AttributeValueTypes.$1"');
|
|
91
91
|
|
|
92
|
-
//
|
|
93
|
-
yamlContent = yamlContent
|
|
92
|
+
// Convert JSON-like double-quoted values to single-quoted and wrap long lines
|
|
93
|
+
yamlContent = patchYamlToPyyaml(yamlContent);
|
|
94
94
|
|
|
95
95
|
// Save all files: attributes.yaml, ID mapping, and backup for diff tracking
|
|
96
96
|
await writeFileSafe(customerAttributesPath(customer.idn), yamlContent);
|
|
@@ -172,20 +172,20 @@ export async function saveProjectAttributes(
|
|
|
172
172
|
attributes: cleanAttributes
|
|
173
173
|
};
|
|
174
174
|
|
|
175
|
-
//
|
|
175
|
+
// Emit YAML without folding/wrapping; patchYamlToPyyaml handles long-line
|
|
176
|
+
// wrapping and converts JSON-like double-quoted values to single-quoted.
|
|
176
177
|
let yamlContent = yaml.dump(attributesYaml, {
|
|
177
178
|
indent: 2,
|
|
178
179
|
quotingType: '"',
|
|
179
180
|
forceQuotes: false,
|
|
180
|
-
lineWidth:
|
|
181
|
+
lineWidth: -1,
|
|
181
182
|
noRefs: true,
|
|
182
183
|
sortKeys: false,
|
|
183
|
-
flowLevel: -1
|
|
184
|
+
flowLevel: -1,
|
|
184
185
|
});
|
|
185
186
|
|
|
186
|
-
// Post-process to fix enum format
|
|
187
187
|
yamlContent = yamlContent.replace(/__ENUM_PLACEHOLDER_(\w+)__/g, '!enum "AttributeValueTypes.$1"');
|
|
188
|
-
yamlContent = yamlContent
|
|
188
|
+
yamlContent = patchYamlToPyyaml(yamlContent);
|
|
189
189
|
|
|
190
190
|
// Save to project directory
|
|
191
191
|
const customerDir = path.join(process.cwd(), 'newo_customers', customer.idn);
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Conversations synchronization module
|
|
3
|
+
*
|
|
4
|
+
* Incremental/resumable conversation pull:
|
|
5
|
+
* - Writes per-persona JSON files to newo_customers/<idn>/conversations/<persona_id>.json as they arrive
|
|
6
|
+
* - Updates conversations.yaml aggregate after each persona finishes
|
|
7
|
+
* - Skips personas already fully fetched (resume support) unless --force passed via env NEWO_CONV_FORCE=1
|
|
8
|
+
* - Graceful on partial failure: individual persona errors do not abort the batch, state is preserved
|
|
3
9
|
*/
|
|
4
10
|
import { listUserPersonas, getChatHistory } from '../api.js';
|
|
5
11
|
import { writeFileSafe } from '../fsutil.js';
|
|
12
|
+
import fs from 'fs-extra';
|
|
13
|
+
import path from 'path';
|
|
6
14
|
import yaml from 'js-yaml';
|
|
7
15
|
import pLimit from 'p-limit';
|
|
8
16
|
import type { AxiosInstance } from 'axios';
|
|
@@ -16,11 +24,108 @@ import type {
|
|
|
16
24
|
ConversationsData
|
|
17
25
|
} from '../types.js';
|
|
18
26
|
|
|
19
|
-
// Concurrency limit for API calls
|
|
20
27
|
const concurrencyLimit = pLimit(5);
|
|
21
28
|
|
|
29
|
+
type PersonaState = {
|
|
30
|
+
id: string;
|
|
31
|
+
name: string;
|
|
32
|
+
phone: string | null;
|
|
33
|
+
act_count: number;
|
|
34
|
+
acts: ProcessedAct[];
|
|
35
|
+
fetched_at: string;
|
|
36
|
+
complete: boolean;
|
|
37
|
+
last_error?: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function personaFilePath(customerIdn: string, personaId: string): string {
|
|
41
|
+
return path.join('newo_customers', customerIdn, 'conversations', `${personaId}.json`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function aggregateYamlPath(customerIdn: string): string {
|
|
45
|
+
return path.join('newo_customers', customerIdn, 'conversations.yaml');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function readPersonaState(customerIdn: string, personaId: string): Promise<PersonaState | null> {
|
|
49
|
+
const p = personaFilePath(customerIdn, personaId);
|
|
50
|
+
if (!(await fs.pathExists(p))) return null;
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(await fs.readFile(p, 'utf8')) as PersonaState;
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function writePersonaState(customerIdn: string, state: PersonaState): Promise<void> {
|
|
59
|
+
await writeFileSafe(personaFilePath(customerIdn, state.id), JSON.stringify(state, null, 2));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function writeAggregateYaml(customerIdn: string): Promise<{ personas: number; acts: number }> {
|
|
63
|
+
const dir = path.join('newo_customers', customerIdn, 'conversations');
|
|
64
|
+
const files = (await fs.pathExists(dir)) ? await fs.readdir(dir) : [];
|
|
65
|
+
const personas: ProcessedPersona[] = [];
|
|
66
|
+
for (const f of files) {
|
|
67
|
+
if (!f.endsWith('.json')) continue;
|
|
68
|
+
try {
|
|
69
|
+
const state = JSON.parse(await fs.readFile(path.join(dir, f), 'utf8')) as PersonaState;
|
|
70
|
+
personas.push({
|
|
71
|
+
id: state.id,
|
|
72
|
+
name: state.name,
|
|
73
|
+
phone: state.phone,
|
|
74
|
+
act_count: state.act_count,
|
|
75
|
+
acts: state.acts
|
|
76
|
+
});
|
|
77
|
+
} catch {
|
|
78
|
+
// skip corrupted file
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
personas.sort((a, b) => {
|
|
83
|
+
const aLatestTime = a.acts.length > 0 ? a.acts[a.acts.length - 1]!.datetime : '1970-01-01T00:00:00.000Z';
|
|
84
|
+
const bLatestTime = b.acts.length > 0 ? b.acts[b.acts.length - 1]!.datetime : '1970-01-01T00:00:00.000Z';
|
|
85
|
+
return new Date(bLatestTime).getTime() - new Date(aLatestTime).getTime();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const totalActs = personas.reduce((sum, p) => sum + p.acts.length, 0);
|
|
89
|
+
|
|
90
|
+
const data: ConversationsData = {
|
|
91
|
+
personas,
|
|
92
|
+
total_personas: personas.length,
|
|
93
|
+
total_acts: totalActs,
|
|
94
|
+
generated_at: new Date().toISOString()
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const yamlContent = yaml.dump(data, {
|
|
98
|
+
indent: 2,
|
|
99
|
+
quotingType: '"',
|
|
100
|
+
forceQuotes: false,
|
|
101
|
+
lineWidth: 120,
|
|
102
|
+
noRefs: true,
|
|
103
|
+
sortKeys: false,
|
|
104
|
+
flowLevel: -1
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await writeFileSafe(aggregateYamlPath(customerIdn), yamlContent);
|
|
108
|
+
return { personas: personas.length, acts: totalActs };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function buildProcessedActs(raw: ConversationAct[]): ProcessedAct[] {
|
|
112
|
+
const sorted = [...raw].sort((a, b) => new Date(a.datetime).getTime() - new Date(b.datetime).getTime());
|
|
113
|
+
return sorted.map(act => {
|
|
114
|
+
const processedAct: ProcessedAct = {
|
|
115
|
+
datetime: act.datetime,
|
|
116
|
+
type: act.reference_idn,
|
|
117
|
+
message: act.source_text
|
|
118
|
+
};
|
|
119
|
+
if (act.contact_information) (processedAct as any).contact_information = act.contact_information;
|
|
120
|
+
if (act.flow_idn && act.flow_idn !== 'unknown') (processedAct as any).flow_idn = act.flow_idn;
|
|
121
|
+
if (act.skill_idn && act.skill_idn !== 'unknown') (processedAct as any).skill_idn = act.skill_idn;
|
|
122
|
+
if (act.session_id && act.session_id !== 'unknown') (processedAct as any).session_id = act.session_id;
|
|
123
|
+
return processedAct;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
22
127
|
/**
|
|
23
|
-
* Pull conversations for a customer and save
|
|
128
|
+
* Pull conversations for a customer and save incrementally.
|
|
24
129
|
*/
|
|
25
130
|
export async function pullConversations(
|
|
26
131
|
client: AxiosInstance,
|
|
@@ -28,230 +133,178 @@ export async function pullConversations(
|
|
|
28
133
|
options: ConversationOptions = {},
|
|
29
134
|
verbose: boolean = false
|
|
30
135
|
): Promise<void> {
|
|
31
|
-
|
|
136
|
+
const force = process.env.NEWO_CONV_FORCE === '1';
|
|
137
|
+
console.log(`š¬ Fetching conversations for ${customer.idn}${force ? ' (force re-fetch)' : ' (resume mode)'}...`);
|
|
138
|
+
|
|
139
|
+
// Ensure output dirs exist
|
|
140
|
+
await fs.ensureDir(path.join('newo_customers', customer.idn, 'conversations'));
|
|
141
|
+
|
|
142
|
+
// 1. Enumerate all personas
|
|
143
|
+
const allPersonas: UserPersona[] = [];
|
|
144
|
+
let page = 1;
|
|
145
|
+
const perPage = 50;
|
|
146
|
+
let hasMore = true;
|
|
147
|
+
|
|
148
|
+
while (hasMore) {
|
|
149
|
+
const response = await listUserPersonas(client, page, perPage);
|
|
150
|
+
allPersonas.push(...response.items);
|
|
151
|
+
if (verbose) console.log(`š Page ${page}: ${response.items.length} personas (${allPersonas.length}/${response.metadata.total})`);
|
|
152
|
+
hasMore = response.items.length === perPage && allPersonas.length < response.metadata.total;
|
|
153
|
+
page++;
|
|
154
|
+
}
|
|
32
155
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
let page = 1;
|
|
37
|
-
const perPage = 50;
|
|
38
|
-
let hasMore = true;
|
|
156
|
+
if (options.maxPersonas && allPersonas.length > options.maxPersonas) {
|
|
157
|
+
allPersonas.splice(options.maxPersonas);
|
|
158
|
+
}
|
|
39
159
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
160
|
+
const total = allPersonas.length;
|
|
161
|
+
console.log(`š„ Found ${total} personas. Processing with concurrency=5...`);
|
|
162
|
+
|
|
163
|
+
let done = 0;
|
|
164
|
+
let skipped = 0;
|
|
165
|
+
let failed = 0;
|
|
166
|
+
|
|
167
|
+
await Promise.all(allPersonas.map(persona => concurrencyLimit(async () => {
|
|
168
|
+
try {
|
|
169
|
+
// Resume: skip if already complete
|
|
170
|
+
const existing = await readPersonaState(customer.idn, persona.id);
|
|
171
|
+
if (!force && existing && existing.complete) {
|
|
172
|
+
skipped++;
|
|
173
|
+
done++;
|
|
174
|
+
if (verbose) console.log(`āļø [${done}/${total}] ${persona.name}: already complete (${existing.acts.length} acts)`);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
43
177
|
|
|
44
|
-
|
|
178
|
+
const phoneActor = persona.actors.find(actor =>
|
|
179
|
+
actor.integration_idn === 'newo_voice' &&
|
|
180
|
+
actor.connector_idn === 'newo_voice_connector' &&
|
|
181
|
+
actor.contact_information?.startsWith('+')
|
|
182
|
+
);
|
|
183
|
+
const phone = phoneActor?.contact_information || null;
|
|
45
184
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
185
|
+
const userActors = persona.actors.filter(actor =>
|
|
186
|
+
actor.integration_idn === 'newo_voice' &&
|
|
187
|
+
actor.connector_idn === 'newo_voice_connector'
|
|
188
|
+
);
|
|
49
189
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
190
|
+
if (userActors.length === 0) {
|
|
191
|
+
const state: PersonaState = {
|
|
192
|
+
id: persona.id,
|
|
193
|
+
name: persona.name,
|
|
194
|
+
phone,
|
|
195
|
+
act_count: persona.act_count,
|
|
196
|
+
acts: [],
|
|
197
|
+
fetched_at: new Date().toISOString(),
|
|
198
|
+
complete: true
|
|
199
|
+
};
|
|
200
|
+
await writePersonaState(customer.idn, state);
|
|
201
|
+
done++;
|
|
202
|
+
if (verbose) console.log(`ā [${done}/${total}] ${persona.name}: no voice actors`);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
54
205
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
// Get acts for this persona
|
|
71
|
-
const allActs: ConversationAct[] = [];
|
|
72
|
-
let actPage = 1;
|
|
73
|
-
const actsPerPage = 100; // Higher limit for acts
|
|
74
|
-
let hasMoreActs = true;
|
|
75
|
-
|
|
76
|
-
// Get user actor IDs from persona actors first
|
|
77
|
-
const userActors = persona.actors.filter(actor =>
|
|
78
|
-
actor.integration_idn === 'newo_voice' &&
|
|
79
|
-
actor.connector_idn === 'newo_voice_connector'
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
if (userActors.length === 0) {
|
|
83
|
-
if (verbose) console.log(` š¤ ${persona.name}: No voice actors found, skipping`);
|
|
84
|
-
// No voice actors, can't get chat history - add persona with empty acts
|
|
85
|
-
processedPersonas.push({
|
|
86
|
-
id: persona.id,
|
|
87
|
-
name: persona.name,
|
|
88
|
-
phone,
|
|
89
|
-
act_count: persona.act_count,
|
|
90
|
-
acts: []
|
|
206
|
+
// Fetch acts paginated
|
|
207
|
+
const allActs: ConversationAct[] = [];
|
|
208
|
+
let actPage = 1;
|
|
209
|
+
const actsPerPage = 100;
|
|
210
|
+
let hasMoreActs = true;
|
|
211
|
+
const maxPages = 50;
|
|
212
|
+
let lastError: string | undefined;
|
|
213
|
+
|
|
214
|
+
while (hasMoreActs && actPage <= maxPages) {
|
|
215
|
+
try {
|
|
216
|
+
const chatResponse = await getChatHistory(client, {
|
|
217
|
+
user_actor_id: userActors[0]!.id,
|
|
218
|
+
page: actPage,
|
|
219
|
+
per: actsPerPage
|
|
91
220
|
});
|
|
92
|
-
if (verbose) console.log(` ā Processed ${persona.name}: 0 acts (no voice actors)`);
|
|
93
|
-
return; // Return from the concurrency function
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Safety mechanism to prevent infinite loops
|
|
97
|
-
const maxPages = 50; // Limit to 50 pages (5000 acts max per persona)
|
|
98
221
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
222
|
+
if (chatResponse.items && chatResponse.items.length > 0) {
|
|
223
|
+
const convertedActs: ConversationAct[] = chatResponse.items.map((item: any) => ({
|
|
224
|
+
id: item.id || `chat_${Math.random()}`,
|
|
225
|
+
command_act_id: null,
|
|
226
|
+
external_event_id: item.external_event_id || 'chat_history',
|
|
227
|
+
arguments: [],
|
|
228
|
+
reference_idn: (item.is_agent === true) ? 'agent_message' : 'user_message',
|
|
229
|
+
runtime_context_id: item.runtime_context_id || 'chat_history',
|
|
230
|
+
source_text: item.payload?.text || item.message || item.content || item.text || '',
|
|
231
|
+
original_text: item.payload?.text || item.message || item.content || item.text || '',
|
|
232
|
+
datetime: item.datetime || item.created_at || item.timestamp || new Date().toISOString(),
|
|
102
233
|
user_actor_id: userActors[0]!.id,
|
|
103
|
-
|
|
104
|
-
|
|
234
|
+
agent_actor_id: null,
|
|
235
|
+
user_persona_id: persona.id,
|
|
236
|
+
user_persona_name: persona.name,
|
|
237
|
+
agent_persona_id: item.agent_persona_id || 'unknown',
|
|
238
|
+
external_id: item.external_id || null,
|
|
239
|
+
integration_idn: 'newo_voice',
|
|
240
|
+
connector_idn: 'newo_voice_connector',
|
|
241
|
+
to_integration_idn: null,
|
|
242
|
+
to_connector_idn: null,
|
|
243
|
+
is_agent: Boolean(item.is_agent === true),
|
|
244
|
+
project_idn: null,
|
|
245
|
+
flow_idn: item.flow_idn || 'unknown',
|
|
246
|
+
skill_idn: item.skill_idn || 'unknown',
|
|
247
|
+
session_id: item.session_id || 'unknown',
|
|
248
|
+
recordings: item.recordings || [],
|
|
249
|
+
contact_information: item.contact_information || null
|
|
250
|
+
}));
|
|
251
|
+
|
|
252
|
+
allActs.push(...convertedActs);
|
|
253
|
+
|
|
254
|
+
// Save partial progress every page
|
|
255
|
+
const partialState: PersonaState = {
|
|
256
|
+
id: persona.id,
|
|
257
|
+
name: persona.name,
|
|
258
|
+
phone,
|
|
259
|
+
act_count: persona.act_count,
|
|
260
|
+
acts: buildProcessedActs(allActs),
|
|
261
|
+
fetched_at: new Date().toISOString(),
|
|
262
|
+
complete: false
|
|
105
263
|
};
|
|
264
|
+
await writePersonaState(customer.idn, partialState);
|
|
106
265
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
// Convert chat history format to acts format - create minimal ConversationAct objects
|
|
112
|
-
const convertedActs: ConversationAct[] = chatResponse.items.map((item: any) => ({
|
|
113
|
-
id: item.id || `chat_${Math.random()}`,
|
|
114
|
-
command_act_id: null,
|
|
115
|
-
external_event_id: item.external_event_id || 'chat_history',
|
|
116
|
-
arguments: [],
|
|
117
|
-
reference_idn: (item.is_agent === true) ? 'agent_message' : 'user_message',
|
|
118
|
-
runtime_context_id: item.runtime_context_id || 'chat_history',
|
|
119
|
-
source_text: item.payload?.text || item.message || item.content || item.text || '',
|
|
120
|
-
original_text: item.payload?.text || item.message || item.content || item.text || '',
|
|
121
|
-
datetime: item.datetime || item.created_at || item.timestamp || new Date().toISOString(),
|
|
122
|
-
user_actor_id: userActors[0]!.id,
|
|
123
|
-
agent_actor_id: null,
|
|
124
|
-
user_persona_id: persona.id,
|
|
125
|
-
user_persona_name: persona.name,
|
|
126
|
-
agent_persona_id: item.agent_persona_id || 'unknown',
|
|
127
|
-
external_id: item.external_id || null,
|
|
128
|
-
integration_idn: 'newo_voice',
|
|
129
|
-
connector_idn: 'newo_voice_connector',
|
|
130
|
-
to_integration_idn: null,
|
|
131
|
-
to_connector_idn: null,
|
|
132
|
-
is_agent: Boolean(item.is_agent === true),
|
|
133
|
-
project_idn: null,
|
|
134
|
-
flow_idn: item.flow_idn || 'unknown',
|
|
135
|
-
skill_idn: item.skill_idn || 'unknown',
|
|
136
|
-
session_id: item.session_id || 'unknown',
|
|
137
|
-
recordings: item.recordings || [],
|
|
138
|
-
contact_information: item.contact_information || null
|
|
139
|
-
}));
|
|
140
|
-
|
|
141
|
-
allActs.push(...convertedActs);
|
|
142
|
-
|
|
143
|
-
if (verbose && convertedActs.length > 0) {
|
|
144
|
-
console.log(` š¤ ${persona.name}: Chat History - ${convertedActs.length} messages (${allActs.length} total)`);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Check if we should continue paginating
|
|
148
|
-
const hasMetadata = chatResponse.metadata?.total !== undefined;
|
|
149
|
-
const currentTotal = chatResponse.metadata?.total || 0;
|
|
150
|
-
|
|
151
|
-
hasMoreActs = chatResponse.items.length === actsPerPage &&
|
|
152
|
-
hasMetadata &&
|
|
153
|
-
allActs.length < currentTotal;
|
|
154
|
-
|
|
155
|
-
actPage++;
|
|
156
|
-
|
|
157
|
-
if (verbose) console.log(` š ${persona.name}: Page ${actPage - 1} done, ${allActs.length}/${currentTotal} total acts`);
|
|
158
|
-
} else {
|
|
159
|
-
// No more items
|
|
160
|
-
hasMoreActs = false;
|
|
161
|
-
if (verbose) console.log(` š ${persona.name}: No more chat history items`);
|
|
162
|
-
}
|
|
163
|
-
} catch (chatError) {
|
|
164
|
-
if (verbose) console.log(` ā ļø Chat history failed for ${persona.name}: ${chatError instanceof Error ? chatError.message : String(chatError)}`);
|
|
266
|
+
const currentTotal = chatResponse.metadata?.total || 0;
|
|
267
|
+
hasMoreActs = chatResponse.items.length === actsPerPage && allActs.length < currentTotal;
|
|
268
|
+
actPage++;
|
|
269
|
+
} else {
|
|
165
270
|
hasMoreActs = false;
|
|
166
271
|
}
|
|
272
|
+
} catch (chatError) {
|
|
273
|
+
lastError = chatError instanceof Error ? chatError.message : String(chatError);
|
|
274
|
+
if (verbose) console.log(`ā ļø ${persona.name} page ${actPage}: ${lastError}`);
|
|
275
|
+
hasMoreActs = false;
|
|
167
276
|
}
|
|
168
|
-
|
|
169
|
-
if (actPage > maxPages) {
|
|
170
|
-
if (verbose) console.log(` ā ļø ${persona.name}: Reached max pages limit (${maxPages}), stopping pagination`);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Sort acts by datetime ascending (chronological order)
|
|
174
|
-
allActs.sort((a, b) => new Date(a.datetime).getTime() - new Date(b.datetime).getTime());
|
|
175
|
-
|
|
176
|
-
// Process acts into simplified format - exclude redundant fields
|
|
177
|
-
const processedActs: ProcessedAct[] = allActs.map(act => {
|
|
178
|
-
const processedAct: ProcessedAct = {
|
|
179
|
-
datetime: act.datetime,
|
|
180
|
-
type: act.reference_idn,
|
|
181
|
-
message: act.source_text
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
// Only include non-redundant fields
|
|
185
|
-
if (act.contact_information) {
|
|
186
|
-
(processedAct as any).contact_information = act.contact_information;
|
|
187
|
-
}
|
|
188
|
-
if (act.flow_idn && act.flow_idn !== 'unknown') {
|
|
189
|
-
(processedAct as any).flow_idn = act.flow_idn;
|
|
190
|
-
}
|
|
191
|
-
if (act.skill_idn && act.skill_idn !== 'unknown') {
|
|
192
|
-
(processedAct as any).skill_idn = act.skill_idn;
|
|
193
|
-
}
|
|
194
|
-
if (act.session_id && act.session_id !== 'unknown') {
|
|
195
|
-
(processedAct as any).session_id = act.session_id;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return processedAct;
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
processedPersonas.push({
|
|
202
|
-
id: persona.id,
|
|
203
|
-
name: persona.name,
|
|
204
|
-
phone,
|
|
205
|
-
act_count: persona.act_count,
|
|
206
|
-
acts: processedActs
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
if (verbose) console.log(` ā Processed ${persona.name}: ${processedActs.length} acts`);
|
|
210
|
-
} catch (error) {
|
|
211
|
-
console.error(`ā Failed to process persona ${persona.name}:`, error);
|
|
212
|
-
// Continue with other personas
|
|
213
277
|
}
|
|
214
|
-
})));
|
|
215
|
-
|
|
216
|
-
// Sort personas by most recent act time (descending) - use latest act from acts array
|
|
217
|
-
processedPersonas.sort((a, b) => {
|
|
218
|
-
const aLatestTime = a.acts.length > 0 ? a.acts[a.acts.length - 1]!.datetime : '1970-01-01T00:00:00.000Z';
|
|
219
|
-
const bLatestTime = b.acts.length > 0 ? b.acts[b.acts.length - 1]!.datetime : '1970-01-01T00:00:00.000Z';
|
|
220
|
-
return new Date(bLatestTime).getTime() - new Date(aLatestTime).getTime();
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
// Calculate totals
|
|
224
|
-
const totalActs = processedPersonas.reduce((sum, persona) => sum + persona.acts.length, 0);
|
|
225
|
-
|
|
226
|
-
// Create final conversations data
|
|
227
|
-
const conversationsData: ConversationsData = {
|
|
228
|
-
personas: processedPersonas,
|
|
229
|
-
total_personas: processedPersonas.length,
|
|
230
|
-
total_acts: totalActs,
|
|
231
|
-
generated_at: new Date().toISOString()
|
|
232
|
-
};
|
|
233
278
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
279
|
+
const finalState: PersonaState = {
|
|
280
|
+
id: persona.id,
|
|
281
|
+
name: persona.name,
|
|
282
|
+
phone,
|
|
283
|
+
act_count: persona.act_count,
|
|
284
|
+
acts: buildProcessedActs(allActs),
|
|
285
|
+
fetched_at: new Date().toISOString(),
|
|
286
|
+
complete: !lastError
|
|
287
|
+
};
|
|
288
|
+
if (lastError) finalState.last_error = lastError;
|
|
289
|
+
await writePersonaState(customer.idn, finalState);
|
|
290
|
+
|
|
291
|
+
// Incremental YAML aggregate every persona
|
|
292
|
+
const agg = await writeAggregateYaml(customer.idn);
|
|
293
|
+
|
|
294
|
+
done++;
|
|
295
|
+
if (lastError) failed++;
|
|
296
|
+
console.log(`${lastError ? 'ā ļø ' : 'ā'} [${done}/${total}] ${persona.name}: ${finalState.acts.length} acts${lastError ? ` (partial: ${lastError})` : ''} | total so far: ${agg.personas} personas / ${agg.acts} acts`);
|
|
297
|
+
} catch (error) {
|
|
298
|
+
failed++;
|
|
299
|
+
done++;
|
|
300
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
301
|
+
console.error(`ā [${done}/${total}] ${persona.name}: ${msg}`);
|
|
251
302
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
257
|
-
}
|
|
303
|
+
})));
|
|
304
|
+
|
|
305
|
+
// Final aggregate write
|
|
306
|
+
const final = await writeAggregateYaml(customer.idn);
|
|
307
|
+
console.log(`\nā
Done. ${final.personas} personas, ${final.acts} acts. Skipped ${skipped} (already cached), ${failed} had errors.`);
|
|
308
|
+
console.log(` Aggregate: ${aggregateYamlPath(customer.idn)}`);
|
|
309
|
+
console.log(` Per-persona: newo_customers/${customer.idn}/conversations/<id>.json`);
|
|
310
|
+
}
|