commit ad127156ff1da6e8f8724d7b73b4f227c9bcff11 Author: jeremy bayse Date: Mon Jun 22 16:40:24 2026 +0200 Initial commit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6df8428 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 + +[{compose,docker-compose}.{yml,yaml}] +indent_size = 4 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c0660ea --- /dev/null +++ b/.env.example @@ -0,0 +1,65 @@ +APP_NAME=Laravel +APP_ENV=local +APP_KEY= +APP_DEBUG=true +APP_URL=http://localhost + +APP_LOCALE=en +APP_FALLBACK_LOCALE=en +APP_FAKER_LOCALE=en_US + +APP_MAINTENANCE_DRIVER=file +# APP_MAINTENANCE_STORE=database + +# PHP_CLI_SERVER_WORKERS=4 + +BCRYPT_ROUNDS=12 + +LOG_CHANNEL=stack +LOG_STACK=single +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +DB_CONNECTION=sqlite +# DB_HOST=127.0.0.1 +# DB_PORT=3306 +# DB_DATABASE=laravel +# DB_USERNAME=root +# DB_PASSWORD= + +SESSION_DRIVER=database +SESSION_LIFETIME=120 +SESSION_ENCRYPT=false +SESSION_PATH=/ +SESSION_DOMAIN=null + +BROADCAST_CONNECTION=log +FILESYSTEM_DISK=local +QUEUE_CONNECTION=database + +CACHE_STORE=database +# CACHE_PREFIX= + +MEMCACHED_HOST=127.0.0.1 + +REDIS_CLIENT=phpredis +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=log +MAIL_SCHEME=null +MAIL_HOST=127.0.0.1 +MAIL_PORT=2525 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_FROM_ADDRESS="hello@example.com" +MAIL_FROM_NAME="${APP_NAME}" + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= +AWS_USE_PATH_STYLE_ENDPOINT=false + +VITE_APP_NAME="${APP_NAME}" diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..fcb21d3 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +* text=auto eol=lf + +*.blade.php diff=html +*.css diff=css +*.html diff=html +*.md diff=markdown +*.php diff=php + +/.github export-ignore +CHANGELOG.md export-ignore +.styleci.yml export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7be55e2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +*.log +.DS_Store +.env +.env.backup +.env.production +.phpactor.json +.phpunit.result.cache +/.codex +/.cursor/ +/.idea +/.nova +/.phpunit.cache +/.vscode +/.zed +/auth.json +/node_modules +/public/build +/public/fonts-manifest.dev.json +/public/hot +/public/storage +/storage/*.key +/storage/pail +/vendor +_ide_helper.php +Homestead.json +Homestead.yaml +Thumbs.db diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..495a6af --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +ignore-scripts=true +audit=true diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md new file mode 100644 index 0000000..2bcd799 --- /dev/null +++ b/DOCUMENTATION.md @@ -0,0 +1,130 @@ +# Documentation Technique - Chatbot RAG Multi-Clients + +Cette documentation explique le fonctionnement global de l'application de chatbot RAG (Retrieval-Augmented Generation) multi-tenants, ainsi que les concepts mathématiques des vecteurs et des embeddings utilisés pour la recherche sémantique. + +--- + +## 1. Architecture Générale (RAG) + +L'application utilise le pattern **RAG (Retrieval-Augmented Generation)**. Contrairement à un chatbot classique qui s'appuie uniquement sur les connaissances générales d'un grand modèle de langage (LLM), le RAG permet d'injecter des données issues de documents propres à l'utilisateur (fichiers PDF, pages de sites web) directement dans le prompt du LLM pour garantir des réponses précises, sourcées et sans hallucinations. + +```mermaid +graph TD + A[Utilisateur pose une question] --> B[Recherche Vectorielle] + B -->|Convertit la question en vecteur| C[Weaviate Cloud] + C -->|Retourne les segments de texte pertinents| D[Prompt RAG] + D -->|Construit le prompt : Contexte + Question| E[Mistral AI] + E -->|Génère la réponse finale| F[Retour à l'utilisateur] +``` + +### Les Composants de l'Application : +1. **Frontend (Livewire & JS Widget)** : Une interface de discussion moderne et réactive qui peut être intégrée sur n'importe quel site internet externe via une simple balise ` + + + @livewireStyles + + + + + + +
+
+ +
+ + +
+
+
+ + + + + Retour à la console + + + + ID: {{ $chatbot->id }} + +
+

+ Configuration : {{ $chatbot->name }} +

+

+ Configurez le prompt, indexez des documents et testez le chatbot de votre client en temps réel. +

+
+ +
+ + + Actif + +
+
+ + +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+

© {{ date('Y') }} Chatbot AGGLO. Panel client indépendant.

+
+ +
+ + @livewireScripts + + diff --git a/resources/views/chatbot-demo.blade.php b/resources/views/chatbot-demo.blade.php new file mode 100644 index 0000000..ffc9e22 --- /dev/null +++ b/resources/views/chatbot-demo.blade.php @@ -0,0 +1,60 @@ + + + + + + Test Chatbot - {{ $chatbot->name }} + + + + + + + + + + + @livewireStyles + + + + + + +
+
+ +
+ +
+ + + + + Retour à la console + + Démonstrateur client ({{ $chatbot->slug }}) +
+ + +
+ + @livewireScripts + + diff --git a/resources/views/chatbot-widget-frame.blade.php b/resources/views/chatbot-widget-frame.blade.php new file mode 100644 index 0000000..a3e8949 --- /dev/null +++ b/resources/views/chatbot-widget-frame.blade.php @@ -0,0 +1,47 @@ + + + + + + Chatbot Widget + + + + + + + + + + + @livewireStyles + + + + +
+ +
+ + @livewireScripts + + diff --git a/resources/views/livewire/admin-dashboard.blade.php b/resources/views/livewire/admin-dashboard.blade.php new file mode 100644 index 0000000..2c45670 --- /dev/null +++ b/resources/views/livewire/admin-dashboard.blade.php @@ -0,0 +1,220 @@ +
+ + +
+
+

Panneau de configuration RAG

+

Indexez des sources et ajustez le comportement du chatbot.

+
+ +
+ + + @if ($statusMessage) +
+ @if($statusType === 'info') + + + + + @else + + + + @endif +
{{ $statusMessage }}
+
+ @endif + + +
+ +
+
+ + + @error('url') {{ $message }} @enderror + +
+
+ + +
+
+ + @if($crawlEntireSite) +
+ + + (max. 100) +
+ @error('maxPages') {{ $message }} @enderror + @endif +
+ +
+ + +
+
+ +
+ +
+ @error('pdfFile') {{ $message }} @enderror + +
+ + +
+
+ +
+
+ + +
+
+
+ + + @error('chatbotName') {{ $message }} @enderror +
+
+
+ + Définit le rôle, les instructions et les limites de l'IA +
+ + @error('systemPrompt') {{ $message }} @enderror +
+
+ +
+ + +
+
+

Script d'intégration

+ Copiez ce script avant la balise </body> de votre site externe +
+
+ <script src="{{ url('/chatbot-widget.js') }}?chatbot={{ $chatbot->slug }}" defer></script> +
+
+ + +
+

Historique des actions

+ + @if(empty($history)) +

Aucune action enregistrée pour le moment.

+ @else +
+ + + + + + + + + + + @foreach(array_reverse($history) as $log) + + + + + + + @endforeach + +
DateActionCibleStatut
{{ $log['timestamp'] }}{{ $log['action'] }}{{ $log['target'] }} + + {{ $log['status'] === 'success' ? 'Succès' : 'Échec' }} + +
+
+ @endif +
+
diff --git a/resources/views/livewire/chatbot-manager.blade.php b/resources/views/livewire/chatbot-manager.blade.php new file mode 100644 index 0000000..4f67483 --- /dev/null +++ b/resources/views/livewire/chatbot-manager.blade.php @@ -0,0 +1,108 @@ +
+ + @if ($statusMessage) +
+ + + +
{{ $statusMessage }}
+
+ @endif + +
+ + +
+

Nouveau Chatbot

+
+
+ + + @error('name') {{ $message }} @enderror +
+ +
+ + +

L'identifiant unique utilisé dans l'URL. Généré automatiquement si laissé vide.

+ @error('slug') {{ $message }} @enderror +
+ + +
+
+ + +
+

Vos Chatbots Actifs

+ + @if($chatbots->isEmpty()) +
+

Aucun chatbot n'a été créé pour le moment. Utilisez le formulaire à gauche pour commencer.

+
+ @else +
+ @foreach($chatbots as $bot) +
+
+
+

{{ $bot->name }}

+ {{ $bot->slug }} +
+

Créé le {{ $bot->created_at->format('d/m/Y à H:i') }}

+
+ +
+ + + +
+
+ @endforeach +
+ @endif +
+ +
+
diff --git a/resources/views/livewire/chatbot.blade.php b/resources/views/livewire/chatbot.blade.php new file mode 100644 index 0000000..af3516c --- /dev/null +++ b/resources/views/livewire/chatbot.blade.php @@ -0,0 +1,119 @@ +
+ +
+
+
+
+ AI +
+ +
+
+

{{ $chatbotName }}

+

En ligne • RAG Activé

+
+
+ + +
+ + +
+ @foreach($messages as $msg) +
+
+ +
+ {!! \Illuminate\Support\Str::markdown($msg['content']) !!} +
+ + @if(isset($msg['sources']) && !empty($msg['sources'])) +
+ Sources consultées : +
+ @foreach($msg['sources'] as $src) + + {{ basename($src) }} + + @endforeach +
+
+ @endif +
+
+ @endforeach + + @if($isSending) +
+
+ + + +
+
+ @endif +
+ + +
+
+ + +
+
+ + @script + + @endscript +
diff --git a/resources/views/welcome.blade.php b/resources/views/welcome.blade.php new file mode 100644 index 0000000..6195b00 --- /dev/null +++ b/resources/views/welcome.blade.php @@ -0,0 +1,97 @@ + + + + + + Chatbot AGGLO - Administration Multi-Clients + + + + + + + + + + + @livewireStyles + + + + + + +
+
+ +
+ + +
+
+
+ + Communauté AGGLO Béziers Méditerranée + +
+

+ Console de gestion des chatbots +

+

+ Gérez et déployez des chatbots RAG indépendants pour chacun de vos sites / Besoins. Alimentés par Weaviate et Mistral AI. +

+
+ +
+ + + RAG Actif + +
+
+ + +
+ +
+ + +
+

© {{ date('Y') }} Chatbot AGGLO SaaS. Construit en Laravel, Weaviate et Mistral AI.

+
+ +
+ + @livewireScripts + + diff --git a/routes/console.php b/routes/console.php new file mode 100644 index 0000000..3c9adf1 --- /dev/null +++ b/routes/console.php @@ -0,0 +1,8 @@ +comment(Inspiring::quote()); +})->purpose('Display an inspiring quote'); diff --git a/routes/web.php b/routes/web.php new file mode 100644 index 0000000..1aca5e7 --- /dev/null +++ b/routes/web.php @@ -0,0 +1,24 @@ += chunk_size: + chunks.append(" ".join(current_chunk)) + + # Slide window back + overlap_words = int(overlap / 6) # Approx 6 chars/word + if overlap_words > 0 and len(current_chunk) > overlap_words: + current_chunk = current_chunk[-overlap_words:] + current_len = len(" ".join(current_chunk)) + else: + current_chunk = [] + current_len = 0 + + if current_chunk: + chunks.append(" ".join(current_chunk)) + + return [c for c in chunks if len(c.strip()) > 30] + +def main(): + if len(sys.argv) < 4: + print(json.dumps({"error": "Missing arguments. Usage: python parse_pdf.py "})) + sys.exit(1) + + pdf_path = sys.argv[1] + api_key = sys.argv[2] + enable_vision = sys.argv[3] == '1' + + if not os.path.exists(pdf_path): + print(json.dumps({"error": f"File not found: {pdf_path}"})) + sys.exit(1) + + filename = os.path.basename(pdf_path) + output_chunks = [] + + try: + doc = fitz.open(pdf_path) + + for page_num in range(len(doc)): + page = doc[page_num] + + # Extract page text preserving block layouts + page_text = page.get_text("blocks") + # Sort blocks top-to-bottom, left-to-right + page_text.sort(key=lambda b: (b[1], b[0])) + + text_lines = [] + for b in page_text: + if len(b) > 4 and isinstance(b[4], str) and b[4].strip(): + text_lines.append(b[4].strip()) + + full_page_text = "\n".join(text_lines) + + # Extract images if vision is enabled + image_descriptions = [] + if enable_vision: + images = page.get_images(full=True) + for img_idx, img in enumerate(images): + xref = img[0] + try: + base_image = doc.extract_image(xref) + image_bytes = base_image["image"] + + # Skip tiny images (e.g., icons, bullets) to save API calls + if len(image_bytes) < 4000: + continue + + desc = extract_image_description(image_bytes, api_key) + if desc: + image_descriptions.append(f"[Illustration Page {page_num+1} - Description: {desc}]") + except Exception as e: + # Log error internally but continue + pass + + # Combine text and image descriptions + combined_text = full_page_text + if image_descriptions: + combined_text += "\n\nDescriptions d'illustrations sur cette page:\n" + "\n".join(image_descriptions) + + # Chunk the page text + page_chunks = chunk_text(combined_text) + + for chunk in page_chunks: + output_chunks.append({ + "content": chunk, + "source": filename, + "title": f"{filename} (Page {page_num+1})" + }) + + doc.close() + print(json.dumps(output_chunks, ensure_ascii=False)) + + except Exception as e: + print(json.dumps({"error": f"Failed to parse PDF: {str(e)}"})) + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/storage/app/.gitignore b/storage/app/.gitignore new file mode 100644 index 0000000..fedb287 --- /dev/null +++ b/storage/app/.gitignore @@ -0,0 +1,4 @@ +* +!private/ +!public/ +!.gitignore diff --git a/storage/app/private/.gitignore b/storage/app/private/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/app/private/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/app/public/.gitignore b/storage/app/public/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/app/public/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/framework/.gitignore b/storage/framework/.gitignore new file mode 100644 index 0000000..05c4471 --- /dev/null +++ b/storage/framework/.gitignore @@ -0,0 +1,9 @@ +compiled.php +config.php +down +events.scanned.php +maintenance.php +routes.php +routes.scanned.php +schedule-* +services.json diff --git a/storage/framework/cache/.gitignore b/storage/framework/cache/.gitignore new file mode 100644 index 0000000..01e4a6c --- /dev/null +++ b/storage/framework/cache/.gitignore @@ -0,0 +1,3 @@ +* +!data/ +!.gitignore diff --git a/storage/framework/cache/data/.gitignore b/storage/framework/cache/data/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/framework/cache/data/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/framework/sessions/.gitignore b/storage/framework/sessions/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/framework/sessions/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/framework/testing/.gitignore b/storage/framework/testing/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/framework/testing/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/framework/views/.gitignore b/storage/framework/views/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/framework/views/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/logs/.gitignore b/storage/logs/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/logs/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/test_ingest.php b/test_ingest.php new file mode 100644 index 0000000..1822cad --- /dev/null +++ b/test_ingest.php @@ -0,0 +1,19 @@ +make('Illuminate\Contracts\Console\Kernel')->bootstrap(); + +$ingestionService = app(App\Services\DocumentIngestionService::class); +$filePath = storage_path('app/private/temp/tiny_test.pdf'); +$originalName = 'tiny_test.pdf'; + +// Let's get the chatbot ID from the slug +$chatbot = App\Models\Chatbot::where('slug', 'dtegdcabm')->first(); +if (!$chatbot) { + echo "Chatbot not found!\n"; + exit(1); +} + +echo "Ingesting tiny_test.pdf for chatbot ID: " . $chatbot->id . "\n"; +$success = $ingestionService->ingestPdf($filePath, $originalName, (string) $chatbot->id, false); +var_dump($success); diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php new file mode 100644 index 0000000..8364a84 --- /dev/null +++ b/tests/Feature/ExampleTest.php @@ -0,0 +1,19 @@ +get('/'); + + $response->assertStatus(200); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..fe1ffc2 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,10 @@ +assertTrue(true); + } +} diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..1fd66d5 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,24 @@ +import { defineConfig } from 'vite'; +import laravel from 'laravel-vite-plugin'; +import { bunny } from 'laravel-vite-plugin/fonts'; +import tailwindcss from '@tailwindcss/vite'; + +export default defineConfig({ + plugins: [ + laravel({ + input: ['resources/css/app.css', 'resources/js/app.js'], + refresh: true, + fonts: [ + bunny('Instrument Sans', { + weights: [400, 500, 600], + }), + ], + }), + tailwindcss(), + ], + server: { + watch: { + ignored: ['**/storage/framework/views/**'], + }, + }, +});