lwazi 1.3.2 → 1.4.1
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/bin/update.js +6 -0
- package/package.json +1 -1
- package/src/Installer/NavigationCrawler.php +24 -3
- package/src/Services/LwaziAgent.php +119 -9
package/bin/update.js
CHANGED
|
@@ -46,6 +46,12 @@ function copyDirectory(src, dest) {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
if (fs.existsSync(targetDir)) {
|
|
50
|
+
console.log("Removing old lwazi directory...");
|
|
51
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log("Copying fresh lwazi files...");
|
|
49
55
|
copyDirectory(packageDir, targetDir);
|
|
50
56
|
|
|
51
57
|
console.log("Running composer update...");
|
package/package.json
CHANGED
|
@@ -99,10 +99,28 @@ class NavigationCrawler
|
|
|
99
99
|
$normalized = $this->normalizeUrl($link['href'], $url);
|
|
100
100
|
if ($this->isInternal($normalized)) {
|
|
101
101
|
$weight = $link['weight'] ?? 1;
|
|
102
|
-
$
|
|
102
|
+
$linkText = $link['text'] ?? '';
|
|
103
103
|
|
|
104
|
-
if (!
|
|
105
|
-
$this->
|
|
104
|
+
if (!in_array($normalized, $this->adjacency[$url], true)) {
|
|
105
|
+
$this->adjacency[$url][] = $normalized;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!isset($this->flatIndex[$normalized])) {
|
|
109
|
+
$this->flatIndex[$normalized] = [
|
|
110
|
+
'label' => $linkText ?: basename($normalized),
|
|
111
|
+
'segments' => [],
|
|
112
|
+
'_path' => $normalized,
|
|
113
|
+
'_weight' => 1,
|
|
114
|
+
'_link_texts' => [],
|
|
115
|
+
];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if ($linkText && !in_array($linkText, $this->flatIndex[$normalized]['_link_texts'] ?? [], true)) {
|
|
119
|
+
$this->flatIndex[$normalized]['_link_texts'][] = $linkText;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if ($weight > ($this->flatIndex[$normalized]['_weight'] ?? 0)) {
|
|
123
|
+
$this->flatIndex[$normalized]['_weight'] = $weight;
|
|
106
124
|
}
|
|
107
125
|
$this->linkWeights[$normalized] = max($this->linkWeights[$normalized], $weight);
|
|
108
126
|
|
|
@@ -262,8 +280,11 @@ class NavigationCrawler
|
|
|
262
280
|
$isMenuLink = isset($menuLinks[$href]);
|
|
263
281
|
$weight = $isMenuLink ? 5 : 1;
|
|
264
282
|
|
|
283
|
+
$linkText = trim($a->textContent ?? '');
|
|
284
|
+
|
|
265
285
|
$links[] = [
|
|
266
286
|
'href' => $href,
|
|
287
|
+
'text' => $linkText,
|
|
267
288
|
'weight' => $weight,
|
|
268
289
|
'is_menu' => $isMenuLink,
|
|
269
290
|
];
|
|
@@ -72,11 +72,12 @@ class LwaziAgent
|
|
|
72
72
|
$rootUrl = $manifest['root_url'] ?? config('app.url', 'http://localhost');
|
|
73
73
|
|
|
74
74
|
if ($intent === 'navigation') {
|
|
75
|
-
$
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
return $this->
|
|
75
|
+
$result = $this->pickNavigationLinkWithFeedback($message, $manifest);
|
|
76
|
+
|
|
77
|
+
if ($result && isset($result['results']) && !empty($result['results'])) {
|
|
78
|
+
return $this->formatNavigationResponse($result, $rootUrl);
|
|
79
79
|
}
|
|
80
|
+
|
|
80
81
|
return $this->generateNavigationHelp($message, $manifest);
|
|
81
82
|
}
|
|
82
83
|
|
|
@@ -87,6 +88,44 @@ class LwaziAgent
|
|
|
87
88
|
return $this->sanitize($this->chat($message));
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
protected function formatNavigationResponse(array $result, string $rootUrl): string
|
|
92
|
+
{
|
|
93
|
+
$results = $result['results'] ?? [];
|
|
94
|
+
$intent = $result['intent'] ?? 'that';
|
|
95
|
+
|
|
96
|
+
if (count($results) === 1) {
|
|
97
|
+
$r = $results[0];
|
|
98
|
+
$fullUrl = $this->buildFullUrl($r['path'], $rootUrl);
|
|
99
|
+
$linkTexts = $r['link_texts'] ?? [];
|
|
100
|
+
|
|
101
|
+
$response = "I found a page for \"{$intent}\":\n\n";
|
|
102
|
+
$response .= "🔗 {$r['label']}: {$fullUrl}\n";
|
|
103
|
+
|
|
104
|
+
if (!empty($linkTexts)) {
|
|
105
|
+
$response .= "\nThis page is linked as: " . implode(', ', array_slice($linkTexts, 0, 5));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return $this->sanitize($response);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
$response = "I found multiple pages that match \"{$intent}\":\n\n";
|
|
112
|
+
|
|
113
|
+
foreach ($results as $i => $r) {
|
|
114
|
+
$fullUrl = $this->buildFullUrl($r['path'], $rootUrl);
|
|
115
|
+
$linkTexts = $r['link_texts'] ?? [];
|
|
116
|
+
|
|
117
|
+
$response .= ($i + 1) . ". **{$r['label']}**\n";
|
|
118
|
+
$response .= " {$fullUrl}\n";
|
|
119
|
+
|
|
120
|
+
if (!empty($linkTexts)) {
|
|
121
|
+
$response .= " Linked as: " . implode(', ', array_slice($linkTexts, 0, 3)) . "\n";
|
|
122
|
+
}
|
|
123
|
+
$response .= "\n";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return $this->sanitize($response);
|
|
127
|
+
}
|
|
128
|
+
|
|
90
129
|
protected function buildFullUrl(string $path, string $rootUrl): string
|
|
91
130
|
{
|
|
92
131
|
if (str_starts_with($path, 'http://') || str_starts_with($path, 'https://')) {
|
|
@@ -126,7 +165,7 @@ class LwaziAgent
|
|
|
126
165
|
return 'general';
|
|
127
166
|
}
|
|
128
167
|
|
|
129
|
-
protected function pickNavigationLinkWithFeedback(string $message, array $manifest): ?
|
|
168
|
+
protected function pickNavigationLinkWithFeedback(string $message, array $manifest): ?array
|
|
130
169
|
{
|
|
131
170
|
$semanticUnderstanding = $this->understandUserIntent($message, $manifest);
|
|
132
171
|
if (!$semanticUnderstanding) {
|
|
@@ -146,12 +185,83 @@ class LwaziAgent
|
|
|
146
185
|
'flat' => $manifest['flat'] ?? [],
|
|
147
186
|
];
|
|
148
187
|
|
|
149
|
-
$
|
|
150
|
-
|
|
151
|
-
|
|
188
|
+
$allResults = $this->searchNavigationTreeAll($message, $semanticUnderstanding, $navigationTree);
|
|
189
|
+
|
|
190
|
+
if (!empty($allResults)) {
|
|
191
|
+
return [
|
|
192
|
+
'results' => $allResults,
|
|
193
|
+
'intent' => $semanticUnderstanding['intent'] ?? null,
|
|
194
|
+
];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
$fallback = $this->searchByContent($message, $semanticUnderstanding, $manifest);
|
|
198
|
+
if ($fallback) {
|
|
199
|
+
return [
|
|
200
|
+
'results' => [$fallback],
|
|
201
|
+
'intent' => $semanticUnderstanding['intent'] ?? null,
|
|
202
|
+
];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
protected function searchNavigationTreeAll(string $message, array $understanding, array $navigationTree): array
|
|
209
|
+
{
|
|
210
|
+
$flatIndex = $navigationTree['flat'] ?? [];
|
|
211
|
+
$keywords = $understanding['keywords'] ?? [];
|
|
212
|
+
$query = $understanding['intent'] ?? $message;
|
|
213
|
+
$results = [];
|
|
214
|
+
|
|
215
|
+
foreach ($flatIndex as $path => $entry) {
|
|
216
|
+
$label = strtolower($entry['label'] ?? '');
|
|
217
|
+
$segments = $entry['segments'] ?? [];
|
|
218
|
+
$linkTexts = $entry['_link_texts'] ?? [];
|
|
219
|
+
|
|
220
|
+
foreach ($keywords as $keyword) {
|
|
221
|
+
$kw = strtolower(trim($keyword));
|
|
222
|
+
$match = false;
|
|
223
|
+
$matchType = '';
|
|
224
|
+
|
|
225
|
+
if (str_contains($label, $kw)) {
|
|
226
|
+
$match = true;
|
|
227
|
+
$matchType = 'label';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (!$match) {
|
|
231
|
+
foreach ($segments as $seg) {
|
|
232
|
+
if (str_contains(strtolower($seg), $kw)) {
|
|
233
|
+
$match = true;
|
|
234
|
+
$matchType = 'segment';
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (!$match) {
|
|
241
|
+
foreach ($linkTexts as $text) {
|
|
242
|
+
if (str_contains(strtolower($text), $kw)) {
|
|
243
|
+
$match = true;
|
|
244
|
+
$matchType = 'link_text';
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if ($match) {
|
|
251
|
+
$results[] = [
|
|
252
|
+
'path' => $path,
|
|
253
|
+
'label' => $entry['label'] ?? basename($path),
|
|
254
|
+
'link_texts' => $linkTexts,
|
|
255
|
+
'match_type' => $matchType,
|
|
256
|
+
'score' => ($entry['_weight'] ?? 1) + ($matchType === 'link_text' ? 2 : 0),
|
|
257
|
+
];
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
152
261
|
}
|
|
153
262
|
|
|
154
|
-
|
|
263
|
+
usort($results, fn($a, $b) => $b['score'] <=> $a['score']);
|
|
264
|
+
return array_slice($results, 0, 10);
|
|
155
265
|
}
|
|
156
266
|
|
|
157
267
|
protected function recordSynonymUsage(array $keywords): void
|