feat: refonte complète vers assistant niveau Iron Man - #1
Conversation
Frontend:
- Zustand store persistant (settings, sessionId, stats)
- Reconnaissance vocale native (Web Speech API)
- Synthèse vocale TTS avec sélection de voix française
- Rendu Markdown complet: bold, italic, code, listes, tableaux, blockquotes
- CommandPalette (Ctrl+K) avec navigation clavier et fuzzy search
- Chat: streaming amélioré, copie de messages, quick commands ML
- Chat: textarea auto-resize, voice visualizer, scroll-to-bottom
- Home: stats live (uptime, connexions, sessions), agents virtuels
- Settings: persistance automatique Zustand, toggles, voix, vitesse
- System: chargement réel des docs markdown depuis le backend
- App: transitions entre pages, raccourcis globaux, toasts stylisés
- CSS: nouvelles animations (glitch-flicker, voice-pulse, border-rotate)
Backend:
- GET /api/status: uptime, connexions, sessions, état des services
- GET /api/docs: liste les fichiers markdown disponibles
- GET /api/docs/{id}: sert le contenu d'un fichier markdown
- Prompt système amélioré style Jarvis avec date/heure dynamiques
- Réponses mock intelligentes par catégorie (heure, statut, aide)
- Session ID = client_id pour persistance cross-refresh
- Compteur de messages et heartbeat amélioré
https://claude.ai/code/session_01QojiFU5TdqBzFEENVaePSK
LLM Multi-Provider (zéro coût): - Ollama: modèles locaux (Llama 3.2, Mistral, Phi-3, DeepSeek, Gemma 2...) → 100% gratuit, privé, offline, aucune limite de requêtes - Groq: cloud gratuit (14 400 req/jour sur Llama 3.3 70B) → Ultra-rapide (LPU), open-source, zéro coût - OpenAI: gardé en fallback optionnel - Auto-détection: Ollama → Groq → OpenAI → Mock Agent avec outils (tous gratuits, sans clé API): - 🔍 search_web: DuckDuckGo (recherche temps réel) - 🌤️ get_weather: wttr.in (météo mondiale) - 🧮 calculate: calculatrice scientifique sécurisée (ast parse) - 📅 get_datetime: date et heure actuelles - 💱 convert_currency: Frankfurter API (taux de change) - 🌐 fetch_url: lecture et résumé de pages web Backend: - GET /api/models: liste Ollama installés + Groq/OpenAI dispo - Auto-initialisation async avec détection Ollama via HTTP - Prompt système dynamique (date/heure) style Jarvis Frontend: - Page /models: guide installation Ollama, modèles recommandés - Badge modèle actif dans Chat (vert=Ollama, orange=Groq, violet=OpenAI) - Provider + model exposés via useAbelChat hook - CommandPalette: commande "Modèles LLM" - Home: bouton raccourci vers /models - .env.example complet avec documentation https://claude.ai/code/session_01QojiFU5TdqBzFEENVaePSK
- start.sh: all-in-one launcher (backend + frontend), shows LAN IP for iPhone - vite.config.ts: host 0.0.0.0 to expose dev server on local network - requirements.txt: use >= versions to resolve pip conflicts (openai, supabase, langchain) - .env.example: full documentation of all config options https://claude.ai/code/session_01QojiFU5TdqBzFEENVaePSK
database.py was creating the Supabase client at module import time, crashing the backend when SUPABASE_URL is empty. Now uses lazy initialization and returns None gracefully when credentials are absent. https://claude.ai/code/session_01QojiFU5TdqBzFEENVaePSK
There was a problem hiding this comment.
Pull request overview
Refonte majeure de l’assistant A.B.E.L côté frontend et backend, avec ajout d’un orchestrateur multi-provider (Ollama/Groq/OpenAI/Mock), d’un système d’outils “agent”, et d’une UI type “command center” (voix, markdown, palette de commandes, pages système/modèles).
Changes:
- Backend: détection automatique du provider LLM, endpoints
/api/status,/api/models,/api/docs, et exécution d’outils (recherche, météo, calcul, etc.). - Frontend: store Zustand persisté (settings/session/stats), chat amélioré (streaming, markdown, copy, TTS/STT), Command Palette (Ctrl+K), pages Home/System/Models enrichies.
- DX/UI: script
start.sh, configuration.env.exampleenrichie, thème/animations CSS étendues, Vite exposé sur le réseau.
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| start.sh | Script de démarrage frontend+backend et affichage URL LAN |
| server/requirements.txt | Ajout dépendances LangChain providers + outils (DDG) et passage en versions >= |
| server/app/services/tools.py | Ajout de la toolbox (search/weather/calc/currency/url) + parsing/exécution |
| server/app/services/brain.py | Orchestration LLM multi-provider + boucle d’outils + streaming |
| server/app/main.py | Nouveaux endpoints status/models/docs + websocket amélioré + métriques |
| server/app/core/database.py | DB optionnelle (Supabase) + health check robuste |
| server/app/core/config.py | Config LLM/providers + flags tools + CORS étendu |
| server/.env.example | Template complet (providers, tools, CORS, sécurité) |
| client/vite.config.ts | Exposition réseau + proxy API/WS + preview host/port |
| client/src/styles/globals.css | Thème/animations “Iron Man edition” + styles prose/chat |
| client/src/store/useStore.ts | Store Zustand persisté (settings + messageCount) + sessionId persistant |
| client/src/pages/System.tsx | Chargement docs depuis backend + statut live + download |
| client/src/pages/Settings.tsx | Settings branchés sur store + toggles + TTS options |
| client/src/pages/Models.tsx | Nouvelle page modèles/providers (Ollama/Groq/OpenAI) |
| client/src/pages/Home.tsx | Dashboard live (status, agents, actions rapides) |
| client/src/pages/Chat.tsx | Chat UI streaming + STT/TTS + markdown + copy + quick commands |
| client/src/hooks/useVoice.ts | Web Speech API (STT+TTS) + nettoyage markdown |
| client/src/hooks/useAbelChat.ts | WS avec session persistée + streaming amélioré + backoff |
| client/src/components/chat/MarkdownMessage.tsx | Renderer markdown custom (inline, blocks, tables, code) |
| client/src/components/CommandPalette.tsx | Palette de commandes (Ctrl/Cmd+K) + navigation clavier |
| client/src/App.tsx | Transitions de pages + Toaster + Command Palette globale + route Models |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def get_supabase_client(): | ||
| """Get Supabase client singleton. Returns None if not configured.""" | ||
| if not _has_supabase_config(): | ||
| return None | ||
| try: | ||
| from supabase import create_client | ||
| from functools import lru_cache | ||
|
|
||
| @lru_cache(maxsize=1) | ||
| def _client(): | ||
| return create_client(settings.SUPABASE_URL, settings.SUPABASE_ANON_KEY) | ||
|
|
||
| class DatabaseError(Exception): | ||
| """Custom database error.""" | ||
| pass | ||
| return _client() | ||
| except Exception: |
There was a problem hiding this comment.
get_supabase_client()/get_supabase_admin() prétendent retourner un singleton mais le @lru_cache est défini à l’intérieur de la fonction : chaque appel recrée une nouvelle fonction + cache, donc le client peut être recréé à chaque fois. Déplacer le cache au niveau module (ou utiliser une variable globale) afin de réellement réutiliser la même instance entre appels.
| async def search_web(query: str, max_results: int = 4) -> str: | ||
| """Recherche sur le web via DuckDuckGo (100% gratuit, sans clé API).""" | ||
| try: | ||
| from duckduckgo_search import DDGS | ||
|
|
||
| results = [] | ||
| with DDGS() as ddgs: | ||
| for r in ddgs.text(query, max_results=max_results, region="fr-fr"): | ||
| results.append(r) | ||
|
|
There was a problem hiding this comment.
search_web() est async mais exécute la recherche DuckDuckGo de manière synchrone (DDGS().text(...)) dans la boucle event-loop. Ça peut bloquer FastAPI pendant la requête réseau/CPU. Exécuter cette partie dans un thread (ex: asyncio.to_thread) ou rendre search_web sync et le faire passer par l’executor dans execute_tool.
| loop = asyncio.get_event_loop() | ||
| return await loop.run_in_executor(None, lambda: tool(**params)) |
There was a problem hiding this comment.
Dans execute_tool(), asyncio.get_event_loop() est déconseillé en contexte async (et peut poser problème selon la policy/versions). Préférer asyncio.get_running_loop() (ou asyncio.to_thread) pour exécuter les outils sync dans un executor.
| loop = asyncio.get_event_loop() | |
| return await loop.run_in_executor(None, lambda: tool(**params)) | |
| return await asyncio.to_thread(lambda: tool(**params)) |
| # First pass: check for tool calls | ||
| first_response = "" | ||
| async for chunk in self._llm.astream(messages): | ||
| if hasattr(chunk, "content") and chunk.content: | ||
| first_response += chunk.content | ||
|
|
||
| tool_calls = parse_tool_calls(first_response) | ||
|
|
||
| if tool_calls: | ||
| # Signal tool usage | ||
| yield f"\n*🔧 Utilisation d'outils...*\n\n" | ||
|
|
||
| # Execute tools | ||
| tool_results = [] | ||
| for call in tool_calls[:settings.MAX_TOOL_ITERATIONS]: | ||
| logger.info(f"Tool: {call['name']} params={call['params']}") | ||
| result = await execute_tool(call["name"], call["params"]) | ||
| tool_results.append(f"[{call['name']}]\n{result}") | ||
| yield f"*✅ {call['name']} terminé*\n" | ||
|
|
||
| # Second pass: synthesize | ||
| clean_first = strip_tool_calls(first_response) | ||
| context_msg = "\n\n".join(tool_results) | ||
| followup = [ | ||
| *messages, | ||
| AIMessage(content=clean_first if clean_first else "Outil exécuté."), | ||
| HumanMessage(content=f"Résultats:\n\n{context_msg}\n\nSynthétise en une réponse claire.") | ||
| ] | ||
| yield "\n" | ||
| full_final = "" | ||
| async for chunk in self._llm.astream(followup): | ||
| if hasattr(chunk, "content") and chunk.content: | ||
| full_final += chunk.content | ||
| yield chunk.content | ||
|
|
||
| self._add_to_history(session_id, "user", message) | ||
| self._add_to_history(session_id, "assistant", full_final) | ||
|
|
||
| else: | ||
| # No tools needed - stream directly | ||
| full_response = "" | ||
| async for chunk in self._llm.astream(messages): | ||
| if hasattr(chunk, "content") and chunk.content: | ||
| full_response += chunk.content | ||
| yield chunk.content | ||
|
|
There was a problem hiding this comment.
stream_message() fait un “first pass” complet pour détecter des tool calls puis relance ensuite un second astream() même quand aucun outil n’est requis (double invocation du modèle, latence et coût x2, et le client ne reçoit rien pendant le first pass). Il faudrait parser les tool calls pendant le streaming (buffer + yield) ou utiliser une unique invocation et décider après coup sans relancer le modèle.
| @app.get("/api/docs/{doc_id}") | ||
| async def get_doc(doc_id: str): | ||
| safe_id = re.sub(r"[^a-zA-Z0-9_\-]", "", doc_id) | ||
| candidates = [ | ||
| DOCS_DIR / f"{safe_id}.md", | ||
| DOCS_DIR / f"{safe_id.upper()}.md", | ||
| ROOT_DIR / f"{safe_id}.md", | ||
| ROOT_DIR / f"{safe_id.upper()}.md", | ||
| ] |
There was a problem hiding this comment.
safe_id peut devenir vide après sanitization (ex: doc_id ne contenant que des caractères filtrés). Dans ce cas on construit des candidats comme .md, ce qui est ambigu et peut mener à des comportements inattendus. Retourner une 400 si safe_id est vide (ou invalide) avant de chercher des fichiers.
| tiktoken>=0.8.0 | ||
|
|
||
| # ─── Agent Tools ────────────────────────────────────────────────────────── | ||
| duckduckgo-search>=7.3.2 # Recherche web gratuite (pas de clé API) |
There was a problem hiding this comment.
Le passage à des contraintes >= (langchain/supabase/openai/...) rend l’environnement non reproductible et risque d’introduire des régressions lors d’installations futures. Garder des versions pinnées (ou introduire un fichier constraints.txt/lock) pour les déploiements, surtout côté backend.
| duckduckgo-search>=7.3.2 # Recherche web gratuite (pas de clé API) | |
| duckduckgo-search==7.3.2 # Recherche web gratuite (pas de clé API) |
| const speak = useCallback((text: string, speed = 1, voiceType = 'default') => { | ||
| if (!('speechSynthesis' in window)) return | ||
|
|
||
| const synth = window.speechSynthesis | ||
| synth.cancel() | ||
|
|
||
| const clean = stripMarkdown(text) | ||
| if (!clean.trim()) return | ||
|
|
||
| const utterance = new SpeechSynthesisUtterance(clean) | ||
| utterance.rate = Math.max(0.5, Math.min(2, speed)) | ||
| utterance.pitch = 1 | ||
| utterance.volume = 1 | ||
| utterance.lang = language | ||
|
|
||
| // Try to select an appropriate voice | ||
| const loadVoices = () => { | ||
| const voices = synth.getVoices() | ||
| if (voices.length > 0) { | ||
| // Prefer French voices | ||
| const frenchVoice = voices.find( | ||
| (v) => v.lang.startsWith('fr') && !v.name.includes('Google') | ||
| ) || voices.find((v) => v.lang.startsWith('fr')) | ||
|
|
||
| if (frenchVoice) { | ||
| utterance.voice = frenchVoice | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (synth.getVoices().length > 0) { | ||
| loadVoices() | ||
| } else { | ||
| synth.onvoiceschanged = loadVoices | ||
| } |
There was a problem hiding this comment.
Le paramètre voiceType de speak() n’est jamais utilisé : l’UI permet de choisir Onyx/Nova/etc mais ça n’influence pas la voix Web Speech. Soit mapper voiceType vers une SpeechSynthesisVoice (par nom/lang), soit supprimer le paramètre/setting pour éviter une option non-fonctionnelle.
| ENABLE_WEB_SEARCH: bool = True # DuckDuckGo (free, no key) | ||
| ENABLE_WEATHER: bool = True # wttr.in (free, no key) | ||
| ENABLE_CALCULATOR: bool = True # Safe Python eval |
There was a problem hiding this comment.
Les flags ENABLE_WEB_SEARCH / ENABLE_WEATHER / ENABLE_CALCULATOR sont exposés dans la config mais ne sont jamais consultés dans l’exécution des outils (tout est toujours disponible dans TOOL_REGISTRY). Soit les appliquer (filtrer/valider dans execute_tool/registry), soit les retirer pour éviter une config trompeuse.
| ENABLE_WEB_SEARCH: bool = True # DuckDuckGo (free, no key) | |
| ENABLE_WEATHER: bool = True # wttr.in (free, no key) | |
| ENABLE_CALCULATOR: bool = True # Safe Python eval |
| @@ -9,62 +9,96 @@ import { | |||
| Moon, | |||
| Sun, | |||
| Save, | |||
| RotateCcw | |||
| RotateCcw, | |||
| Mic, | |||
| Zap, | |||
| Eye, | |||
| Clock, | |||
| Languages, | |||
| } from 'lucide-react' | |||
There was a problem hiding this comment.
Imports inutilisés : useEffect n’est pas utilisé et plusieurs icônes (Mic, Zap, Eye, Clock) ne semblent pas référencées dans le composant. À nettoyer pour éviter warnings/erreurs de lint et réduire le bundle.
| const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' | ||
| const host = window.location.hostname | ||
| const port = import.meta.env.VITE_BACKEND_PORT || '8000' | ||
| // In dev, backend is proxied via vite — use relative path |
There was a problem hiding this comment.
Le commentaire dit « en dev, backend est proxifié via Vite — use relative path », mais getWsUrl() construit en dev une URL directe ws://${host}:${port} (pas relative, pas de proxy). Corriger le commentaire ou utiliser effectivement une URL relative (ex: ${protocol}//${window.location.host}/ws/...) pour éviter la confusion.
| // In dev, backend is proxied via vite — use relative path | |
| // In dev, connect directly to the backend host/port configured for the API. |
Frontend:
Backend:
https://claude.ai/code/session_01QojiFU5TdqBzFEENVaePSK