lwazi 1.8.4 → 1.8.5
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/package.json
CHANGED
|
@@ -202,7 +202,7 @@ class AnalyzeProjectCommand extends Command
|
|
|
202
202
|
|
|
203
203
|
$manifest['flat'][$fullUrl] = [
|
|
204
204
|
'label' => $text ?: basename($fullUrl),
|
|
205
|
-
'segments' => array_filter(explode('/', parse_url($fullUrl,
|
|
205
|
+
'segments' => array_filter(explode('/', parse_url($fullUrl, PHP_URL_PATH) ?? '')),
|
|
206
206
|
'_path' => $fullUrl,
|
|
207
207
|
'_weight' => 1,
|
|
208
208
|
];
|
|
@@ -210,7 +210,7 @@ class SetupCommand extends Command
|
|
|
210
210
|
|
|
211
211
|
$manifest['flat'][$fullUrl] = [
|
|
212
212
|
'label' => $text ?: basename($fullUrl),
|
|
213
|
-
'segments' => array_filter(explode('/', parse_url($fullUrl,
|
|
213
|
+
'segments' => array_filter(explode('/', parse_url($fullUrl, PHP_URL_PATH) ?? '')),
|
|
214
214
|
'_path' => $fullUrl,
|
|
215
215
|
'_weight' => 1,
|
|
216
216
|
];
|
|
@@ -38,6 +38,13 @@ class LwaziService
|
|
|
38
38
|
{
|
|
39
39
|
error_log('Lwazi chat: ' . $message);
|
|
40
40
|
|
|
41
|
+
// Extract context from message if user provides identifier
|
|
42
|
+
$extractedContext = $this->extractContextFromMessage($message);
|
|
43
|
+
if ($extractedContext && empty($this->currentContextId)) {
|
|
44
|
+
$this->currentContextId = $extractedContext;
|
|
45
|
+
error_log('Lwazi: extracted context: ' . $extractedContext);
|
|
46
|
+
}
|
|
47
|
+
|
|
41
48
|
// Temporarily disable agent
|
|
42
49
|
// if ($this->agent->isReady()) {
|
|
43
50
|
// return $this->agent->reply($message, $this->currentContextId);
|
|
@@ -100,6 +107,53 @@ class LwaziService
|
|
|
100
107
|
return "I couldn't find information about that. Could you try a different question?";
|
|
101
108
|
}
|
|
102
109
|
|
|
110
|
+
// Check for general website questions BEFORE falling back to LLM
|
|
111
|
+
if ($intent === 'general' || $this->isGeneralWebsiteQuery($message)) {
|
|
112
|
+
// Simple greeting response first
|
|
113
|
+
if (preg_match('/^(hi|hello|hey|good morning|good afternoon|good evening|how are you)\b/i', $message)) {
|
|
114
|
+
$response = "Hello! I'm here to help you navigate this website. You can ask me things like 'where can I find events' or 'what is this website about'. What would you like to know?";
|
|
115
|
+
$this->conversationHistory[] = ['role' => 'assistant', 'content' => $response];
|
|
116
|
+
return $response;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
$summary = $this->getWebsiteSummary($message);
|
|
120
|
+
if ($summary) {
|
|
121
|
+
$this->conversationHistory[] = ['role' => 'assistant', 'content' => $summary];
|
|
122
|
+
return $summary;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Try content search as fallback
|
|
126
|
+
$contentResponse = $this->searchContent($message);
|
|
127
|
+
if ($contentResponse) {
|
|
128
|
+
$this->conversationHistory[] = ['role' => 'assistant', 'content' => $contentResponse];
|
|
129
|
+
return $contentResponse;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if ($intent === null && $this->currentContextId) {
|
|
134
|
+
$dataResponse = $this->fetchRelevantData($message);
|
|
135
|
+
if ($dataResponse) {
|
|
136
|
+
$this->conversationHistory[] = ['role' => 'assistant', 'content' => $dataResponse];
|
|
137
|
+
return $dataResponse;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Try content search as final fallback before LLM
|
|
142
|
+
$contentResponse = $this->searchContent($message);
|
|
143
|
+
if ($contentResponse) {
|
|
144
|
+
$this->conversationHistory[] = ['role' => 'assistant', 'content' => $contentResponse];
|
|
145
|
+
return $contentResponse;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Last resort: check if this could be a content query and show what's available
|
|
149
|
+
if (!$this->looksLikeDataQuery($message)) {
|
|
150
|
+
$navResponse = $this->getNavigationSuggestions($message);
|
|
151
|
+
if ($navResponse) {
|
|
152
|
+
$this->conversationHistory[] = ['role' => 'assistant', 'content' => $navResponse];
|
|
153
|
+
return $navResponse;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
103
157
|
$prompt = $this->buildPrompt();
|
|
104
158
|
$messages = array_merge(
|
|
105
159
|
[['role' => 'system', 'content' => $prompt]],
|
|
@@ -118,6 +172,10 @@ class LwaziService
|
|
|
118
172
|
return 'data';
|
|
119
173
|
}
|
|
120
174
|
|
|
175
|
+
if ($this->isGeneralWebsiteQuery($message)) {
|
|
176
|
+
return 'general';
|
|
177
|
+
}
|
|
178
|
+
|
|
121
179
|
if ($this->looksLikeNavigationQuery($message) && !$this->looksLikeContentQuery($message)) {
|
|
122
180
|
return 'navigation';
|
|
123
181
|
}
|
|
@@ -128,6 +186,11 @@ class LwaziService
|
|
|
128
186
|
return 'content';
|
|
129
187
|
}
|
|
130
188
|
|
|
189
|
+
// Check for greetings and casual conversation
|
|
190
|
+
if (preg_match('/^(hi|hello|hey|good morning|good afternoon|good evening|how are you|thanks|thank you)\b/i', $msgLower)) {
|
|
191
|
+
return 'general';
|
|
192
|
+
}
|
|
193
|
+
|
|
131
194
|
$prompt =
|
|
132
195
|
"Classify the user's intent as one of: navigation, content, data, general. Return JSON: {\"intent\":\"navigation|content|data|general\"}.\n\n" .
|
|
133
196
|
"- navigation: user wants to find a specific page or link (where, which page, how do i get to)\n" .
|
|
@@ -161,9 +224,11 @@ class LwaziService
|
|
|
161
224
|
$msgLower = strtolower($message);
|
|
162
225
|
|
|
163
226
|
$personalPatterns = [
|
|
164
|
-
'/\bmy\b/', '/\
|
|
227
|
+
'/\bmy\b/', '/\bmine\b/', '/\bmy own\b/',
|
|
165
228
|
'/\bmy account\b/', '/\bmy profile\b/', '/\bmy data\b/',
|
|
166
229
|
'/\bmy records\b/', '/\bmy applications\b/', '/\bmy submissions\b/',
|
|
230
|
+
'/\bmy courses?\b/', '/\bmy results?\b/', '/\bmy fees\b/',
|
|
231
|
+
'/\bmy grades?\b/', '/\bmy profile\b/', '/\bmy information\b/',
|
|
167
232
|
];
|
|
168
233
|
|
|
169
234
|
foreach ($personalPatterns as $pattern) {
|
|
@@ -175,6 +240,46 @@ class LwaziService
|
|
|
175
240
|
return false;
|
|
176
241
|
}
|
|
177
242
|
|
|
243
|
+
protected function isGeneralWebsiteQuery(string $message): bool
|
|
244
|
+
{
|
|
245
|
+
$patterns = [
|
|
246
|
+
'/\bwhat.*this (site|website|app|platform)\b/i',
|
|
247
|
+
'/\babout (this|the) (site|website|app|platform)\b/i',
|
|
248
|
+
'/\bwhat.*(do you|does this).*(offer|provide|have)\b/i',
|
|
249
|
+
'/\btell me about (this|the) (site|website|app)\b/i',
|
|
250
|
+
'/\bwhat is (this|it)\b/i',
|
|
251
|
+
'/\bwhat can (i|you) do (here|on this)\b/i',
|
|
252
|
+
'/\bhow (does|does this) (work|site work)\b/i',
|
|
253
|
+
];
|
|
254
|
+
|
|
255
|
+
foreach ($patterns as $pattern) {
|
|
256
|
+
if (preg_match($pattern, $message)) {
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
protected function getWebsiteSummary(string $message): ?string
|
|
265
|
+
{
|
|
266
|
+
// For now, provide a helpful generic response based on navigation
|
|
267
|
+
$tree = $this->ragService->getNavigationTree();
|
|
268
|
+
if ($tree) {
|
|
269
|
+
$flat = $tree->getFlatIndex();
|
|
270
|
+
$pages = array_keys($flat);
|
|
271
|
+
|
|
272
|
+
if (!empty($pages)) {
|
|
273
|
+
$pageList = array_slice($pages, 0, 8);
|
|
274
|
+
$list = implode(', ', array_map(fn($p) => str_replace('-', ' ', trim($p, '/')), $pageList));
|
|
275
|
+
|
|
276
|
+
return "This appears to be a web application with pages like: {$list}. You can navigate to any of these pages, or ask me to help you find specific content. Use the menu or tell me what you're looking for!";
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return "This is a web application. You can use the navigation menu to find pages, or ask me to help you locate specific content. What would you like to find?";
|
|
281
|
+
}
|
|
282
|
+
|
|
178
283
|
protected function looksLikeContentQuery(string $message): bool
|
|
179
284
|
{
|
|
180
285
|
return (bool) preg_match(
|
|
@@ -183,6 +288,26 @@ class LwaziService
|
|
|
183
288
|
);
|
|
184
289
|
}
|
|
185
290
|
|
|
291
|
+
protected function extractContextFromMessage(string $message): ?string
|
|
292
|
+
{
|
|
293
|
+
$contextParam = config('lwazi.context_param', 'context_id');
|
|
294
|
+
|
|
295
|
+
if (str_contains($contextParam, 'student')) {
|
|
296
|
+
if (preg_match('/\b(STU\d+)\b/i', $message, $matches)) {
|
|
297
|
+
return $matches[1];
|
|
298
|
+
}
|
|
299
|
+
if (preg_match('/\b(\d{6,})\b/', $message, $matches)) {
|
|
300
|
+
return $matches[1];
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (preg_match('/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i', $message, $matches)) {
|
|
305
|
+
return strtolower($matches[0]);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
|
|
186
311
|
public function looksLikeNavigationQuery(string $message): bool
|
|
187
312
|
{
|
|
188
313
|
return (bool) preg_match(
|
|
@@ -304,50 +429,139 @@ class LwaziService
|
|
|
304
429
|
$response = "I found some relevant information:\n\n";
|
|
305
430
|
|
|
306
431
|
foreach ($results as $result) {
|
|
307
|
-
$
|
|
432
|
+
$url = $result['url'] ?? '/';
|
|
433
|
+
// Try to match with navigation for better URL
|
|
434
|
+
$navMatch = $this->getMatchingNavPath($url);
|
|
435
|
+
if ($navMatch) {
|
|
436
|
+
$url = $navMatch;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
$title = $result['title'] ?? basename($url);
|
|
440
|
+
$response .= "**{$title}**\n";
|
|
308
441
|
$response .= "{$result['snippet']}\n";
|
|
309
|
-
$response .= "[
|
|
442
|
+
$response .= "[View page]({$url})\n\n";
|
|
310
443
|
}
|
|
311
444
|
|
|
312
445
|
return $response;
|
|
313
446
|
}
|
|
314
447
|
|
|
448
|
+
protected function getMatchingNavPath(string $contentUrl): ?string
|
|
449
|
+
{
|
|
450
|
+
$tree = $this->ragService->getNavigationTree();
|
|
451
|
+
if (!$tree) {
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
$flat = $tree->getFlatIndex();
|
|
456
|
+
|
|
457
|
+
// Try exact match first
|
|
458
|
+
if (isset($flat[$contentUrl])) {
|
|
459
|
+
return $contentUrl;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Try fuzzy match
|
|
463
|
+
$contentSlug = basename($contentUrl);
|
|
464
|
+
foreach ($flat as $path => $entry) {
|
|
465
|
+
$pathSlug = basename($path);
|
|
466
|
+
if (levenshtein(strtolower($contentSlug), strtolower($pathSlug)) <= 3) {
|
|
467
|
+
return $path;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
protected function getNavigationSuggestions(string $message): ?string
|
|
475
|
+
{
|
|
476
|
+
$tree = $this->ragService->getNavigationTree();
|
|
477
|
+
if (!$tree) {
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
$flat = $tree->getFlatIndex();
|
|
482
|
+
if (empty($flat)) {
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Get a few random pages to suggest
|
|
487
|
+
$pages = array_keys($flat);
|
|
488
|
+
$suggestions = array_slice($pages, 0, 5);
|
|
489
|
+
|
|
490
|
+
if (empty($suggestions)) {
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
$list = [];
|
|
495
|
+
foreach ($suggestions as $path) {
|
|
496
|
+
$label = $flat[$path]['label'] ?? basename($path);
|
|
497
|
+
$list[] = "- {$label}: {$path}";
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return "Here are some pages on this site:\n" . implode("\n", $list) . "\n\nYou can navigate to any of these, or ask me to help you find something specific.";
|
|
501
|
+
}
|
|
502
|
+
|
|
315
503
|
protected function extractSearchTerms(string $message): array
|
|
316
504
|
{
|
|
317
|
-
|
|
318
|
-
$topic = $matches[2] ?? '';
|
|
505
|
+
$msgLower = strtolower($message);
|
|
319
506
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
507
|
+
// Extract topic from common question patterns
|
|
508
|
+
$patterns = [
|
|
509
|
+
'/\babout\s+(\w+)/i',
|
|
510
|
+
'/\b(?:is there|are there|can i find)\s+(?:any\s+)?(\w+)/i',
|
|
511
|
+
'/\binfo(?:rmation)?\s+about\s+(\w+)/i',
|
|
512
|
+
'/\btell me (?:about|the)\s+(\w+)/i',
|
|
513
|
+
];
|
|
514
|
+
|
|
515
|
+
$topics = [];
|
|
516
|
+
foreach ($patterns as $pattern) {
|
|
517
|
+
if (preg_match($pattern, $msgLower, $matches)) {
|
|
518
|
+
if (strlen($matches[1]) >= 3) {
|
|
519
|
+
$topics[] = $matches[1];
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (empty($topics)) {
|
|
525
|
+
// Look for known keywords in the message
|
|
526
|
+
$keywords = ['alumni', 'event', 'events', 'news', 'scholarship', 'scholarships', 'certificate', 'certificates', 'profile', 'profiles', 'course', 'courses', 'result', 'results', 'fee', 'fees', 'benefit', 'benefits', 'story', 'stories'];
|
|
527
|
+
foreach ($keywords as $kw) {
|
|
528
|
+
if (str_contains($msgLower, $kw)) {
|
|
529
|
+
$topics[] = $kw;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (empty($topics)) {
|
|
535
|
+
// Fall back to extracting significant words
|
|
536
|
+
$words = preg_split('/\s+/', $msgLower);
|
|
537
|
+
$stopWords = ['where', 'can', 'find', 'get', 'look', 'show', 'list', 'tell', 'know', 'want', 'need', 'help', 'give', 'me', 'i', 'is', 'are', 'was', 'were', 'has', 'have', 'had', 'do', 'does', 'did', 'will', 'would', 'could', 'should', 'what', 'how', 'why', 'there', 'here', 'this', 'that', 'with', 'from', 'your', 'you', 'for', 'about', 'any'];
|
|
323
538
|
foreach ($words as $w) {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
539
|
+
$clean = preg_replace('/[^a-z]/', '', $w);
|
|
540
|
+
if (strlen($clean) >= 4 && !in_array($clean, $stopWords)) {
|
|
541
|
+
$topics[] = $clean;
|
|
542
|
+
if (count($topics) >= 2) break;
|
|
327
543
|
}
|
|
328
544
|
}
|
|
329
545
|
}
|
|
330
546
|
|
|
331
|
-
if (empty($
|
|
547
|
+
if (empty($topics)) {
|
|
332
548
|
return [];
|
|
333
549
|
}
|
|
334
|
-
|
|
335
|
-
$prompt = "List 5 synonyms for: {$topic}. Return ONLY JSON array like: [\"word1\",\"word2\"]";
|
|
336
|
-
|
|
337
|
-
$response = $this->callOllama([
|
|
338
|
-
['role' => 'system', 'content' => 'Return only valid JSON.'],
|
|
339
|
-
['role' => 'user', 'content' => $prompt],
|
|
340
|
-
]);
|
|
341
|
-
|
|
342
|
-
$json = $this->extractJson($response['content'] ?? '');
|
|
343
|
-
$terms = is_array($json) ? array_values($json) : [];
|
|
344
550
|
|
|
345
|
-
$
|
|
346
|
-
|
|
347
|
-
|
|
551
|
+
$topic = $topics[0];
|
|
552
|
+
|
|
553
|
+
// Build search terms including the original topic
|
|
554
|
+
$terms = array_filter([strtolower($topic)], fn($t) => strlen($t) >= 3);
|
|
555
|
+
|
|
556
|
+
// Add common variations
|
|
557
|
+
$terms[] = strtolower($topic);
|
|
558
|
+
if (str_ends_with($topic, 's')) {
|
|
559
|
+
$terms[] = rtrim($topic, 's');
|
|
560
|
+
} else {
|
|
561
|
+
$terms[] = $topic . 's';
|
|
348
562
|
}
|
|
349
|
-
|
|
350
|
-
return $terms;
|
|
563
|
+
|
|
564
|
+
return array_values(array_unique($terms));
|
|
351
565
|
}
|
|
352
566
|
|
|
353
567
|
protected function findBestRouteWithTerms(array $routes, array $terms): ?string
|