Compare commits

...

2 Commits

Author SHA1 Message Date
arvinxx 1290f79870 update 2025-10-21 14:43:53 +08:00
claude[bot] eb6fd854ef feat: implement auto-suggestion feature for chat messages
- Add auto-suggestion configuration to agent settings
- Create suggestion generation logic with contextual awareness
- Implement UI components for displaying clickable suggestions
- Integrate suggestions into chat message flow
- Add agent settings tab for configuring auto-suggestions
- Support custom prompts and configurable suggestion count

Closes #889

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Arvin Xu <arvinxx@users.noreply.github.com>
2025-10-21 14:31:23 +08:00
76 changed files with 3315 additions and 56 deletions
+7
View File
@@ -14,6 +14,10 @@
"thought": "عملية التفكير",
"unknownTitle": "عمل غير مسمى"
},
"autoSuggestions": {
"generating": "جارٍ إنشاء الاقتراحات...",
"title": "الأسئلة المقترحة"
},
"availableAgents": "المساعدون المتاحون",
"backToBottom": "العودة إلى الأسفل",
"chatList": {
@@ -323,6 +327,9 @@
"loading": "جارٍ التعرف...",
"prettifying": "جارٍ التجميل..."
},
"suggestions": {
"title": "جرّب أن تسأل أيضًا:"
},
"supervisor": {
"todoList": {
"allComplete": "تم إنجاز جميع المهام",
+25
View File
@@ -2,7 +2,27 @@
"about": {
"title": "حول"
},
"agent": {
"autoSuggestion": {
"customPrompt": {
"desc": "إرشاد الذكاء الاصطناعي حول نوع الأسئلة التي يجب توليدها",
"placeholder": "مثال: التركيز على التفاصيل التقنية للتنفيذ...",
"title": "موجه مخصص"
},
"enabled": {
"desc": "توليد اقتراحات للأسئلة التالية تلقائيًا بعد رد المساعد",
"title": "تفعيل الاقتراحات التلقائية"
},
"maxSuggestions": {
"desc": "عدد الاقتراحات المعروضة (1-3)",
"title": "عدد الاقتراحات"
},
"submit": "تحديث الإعدادات",
"title": "الاقتراحات التلقائية"
}
},
"agentTab": {
"autoSuggestion": "الاقتراحات التلقائية",
"chat": "تفضيلات الدردشة",
"meta": "معلومات المساعد",
"modal": "إعدادات النموذج",
@@ -616,6 +636,11 @@
"modelDesc": "يحدد النموذج المستخدم لإنشاء اسم المساعد ووصفه وصورته وعلامته",
"title": "توليد معلومات المساعد تلقائيًا"
},
"autoSuggestion": {
"label": "نموذج الاقتراح الذكي",
"modelDesc": "تحديد النموذج المستخدم لتوليد اقتراحات ذكية للرسائل",
"title": "اقتراحات الرسائل الذكية"
},
"customPrompt": {
"addPrompt": "إضافة موجه مخصص",
"desc": "بعد ملئه، سيستخدم المساعد النظامي الموجه المخصص عند إنشاء المحتوى",
+7
View File
@@ -14,6 +14,10 @@
"thought": "Процес на мислене",
"unknownTitle": "Неназован артефакт"
},
"autoSuggestions": {
"generating": "Генериране на предложения...",
"title": "Предложени въпроси"
},
"availableAgents": "Налични асистенти",
"backToBottom": "Върни се в началото",
"chatList": {
@@ -323,6 +327,9 @@
"loading": "Разпознаване...",
"prettifying": "Изглаждане..."
},
"suggestions": {
"title": "Опитайте да попитате още:"
},
"supervisor": {
"todoList": {
"allComplete": "Всички задачи са изпълнени",
+25
View File
@@ -2,7 +2,27 @@
"about": {
"title": "Относно"
},
"agent": {
"autoSuggestion": {
"customPrompt": {
"desc": "Насочване на AI какъв тип въпроси да генерира",
"placeholder": "Например: съсредоточаване върху технически детайли на реализацията...",
"title": "Персонализирана подсказка"
},
"enabled": {
"desc": "Автоматично генериране на предложения за последващи въпроси след отговор от асистента",
"title": "Активиране на автоматични предложения"
},
"maxSuggestions": {
"desc": "Брой показвани предложения (1-3)",
"title": "Брой предложения"
},
"submit": "Актуализирай настройките",
"title": "Автоматични предложения"
}
},
"agentTab": {
"autoSuggestion": "Автоматични предложения",
"chat": "Предпочитания за чат",
"meta": "Информация за асистента",
"modal": "Настройки на модела",
@@ -616,6 +636,11 @@
"modelDesc": "Модел, определен за генериране на име, описание, профилна снимка и етикети на помощник",
"title": "Автоматично генериране на информация за помощник"
},
"autoSuggestion": {
"label": "Модел за интелигентни предложения",
"modelDesc": "Определяне на модела, използван за генериране на интелигентни предложения за съобщения",
"title": "Интелигентни предложения за съобщения"
},
"customPrompt": {
"addPrompt": "Добавяне на персонализиран подканва",
"desc": "След попълване, системният асистент ще използва персонализираната подканва при генериране на съдържание",
+7
View File
@@ -14,6 +14,10 @@
"thought": "Denkenprozess",
"unknownTitle": "Unbenanntes Werk"
},
"autoSuggestions": {
"generating": "Vorschläge werden generiert...",
"title": "Vorgeschlagene Fragen"
},
"availableAgents": "Verfügbare Assistenten",
"backToBottom": "Zurück zum Ende",
"chatList": {
@@ -323,6 +327,9 @@
"loading": "Erkenne...",
"prettifying": "Verschönern..."
},
"suggestions": {
"title": "Versuchen Sie weiter zu fragen:"
},
"supervisor": {
"todoList": {
"allComplete": "Alle Aufgaben erledigt",
+25
View File
@@ -2,7 +2,27 @@
"about": {
"title": "Über"
},
"agent": {
"autoSuggestion": {
"customPrompt": {
"desc": "Anleitung für die KI, welche Art von Fragen generiert werden sollen",
"placeholder": "Zum Beispiel: Fokus auf technische Implementierungsdetails...",
"title": "Benutzerdefinierte Eingabeaufforderung"
},
"enabled": {
"desc": "Automatische Generierung von Folgefragen nach der Antwort des Assistenten",
"title": "Automatische Vorschläge aktivieren"
},
"maxSuggestions": {
"desc": "Anzahl der angezeigten Vorschläge (13)",
"title": "Anzahl der Vorschläge"
},
"submit": "Einstellungen aktualisieren",
"title": "Automatische Vorschläge"
}
},
"agentTab": {
"autoSuggestion": "Automatische Vorschläge",
"chat": "Chat-Präferenz",
"meta": "Assistenteninformation",
"modal": "Modell-Einstellungen",
@@ -616,6 +636,11 @@
"modelDesc": "Das Modell, das zur Generierung von Assistentennamen, -beschreibungen, -avatars und -tags verwendet wird",
"title": "Automatische Generierung von Assistenteninformationen"
},
"autoSuggestion": {
"label": "Intelligentes Vorschlagsmodell",
"modelDesc": "Modell zur Generierung intelligenter Nachrichten-Vorschläge festlegen",
"title": "Intelligente Nachrichten-Vorschläge"
},
"customPrompt": {
"addPrompt": "Benutzerdefinierte Eingabe hinzufügen",
"desc": "Nachdem Sie dies ausgefüllt haben, verwendet der Systemassistent die benutzerdefinierte Eingabe zur Generierung von Inhalten",
+7
View File
@@ -14,6 +14,10 @@
"thought": "Thought",
"unknownTitle": "Untitled Work"
},
"autoSuggestions": {
"generating": "Generating suggestions...",
"title": "Suggested Questions"
},
"availableAgents": "Available assistants",
"backToBottom": "Back to bottom",
"chatList": {
@@ -323,6 +327,9 @@
"loading": "Recognizing...",
"prettifying": "Polishing..."
},
"suggestions": {
"title": "Try asking:"
},
"supervisor": {
"todoList": {
"allComplete": "All tasks completed",
+25
View File
@@ -2,7 +2,27 @@
"about": {
"title": "About"
},
"agent": {
"autoSuggestion": {
"customPrompt": {
"desc": "Guide the AI on what type of questions to generate",
"placeholder": "e.g., focus on technical implementation details...",
"title": "Custom Prompt"
},
"enabled": {
"desc": "Automatically generate follow-up question suggestions after the assistant responds",
"title": "Enable Auto Suggestions"
},
"maxSuggestions": {
"desc": "Number of suggestions to display (1-3)",
"title": "Suggestion Count"
},
"submit": "Update Settings",
"title": "Auto Suggestions"
}
},
"agentTab": {
"autoSuggestion": "Auto Suggestions",
"chat": "Chat Preferences",
"meta": "Assistant Info",
"modal": "Model Settings",
@@ -616,6 +636,11 @@
"modelDesc": "Model designated for generating assistant name, description, avatar, and tags",
"title": "Automatically Generate Assistant Information"
},
"autoSuggestion": {
"label": "Smart Suggestion Model",
"modelDesc": "Specify the model used to generate smart message suggestions",
"title": "Message Smart Suggestions"
},
"customPrompt": {
"addPrompt": "Add Custom Prompt",
"desc": "Once filled out, the system assistant will use the custom prompt when generating content",
+7
View File
@@ -14,6 +14,10 @@
"thought": "Proceso de pensamiento",
"unknownTitle": "Obra sin título"
},
"autoSuggestions": {
"generating": "Generando sugerencias...",
"title": "Preguntas sugeridas"
},
"availableAgents": "Agentes disponibles",
"backToBottom": "Volver al fondo",
"chatList": {
@@ -323,6 +327,9 @@
"loading": "Reconociendo...",
"prettifying": "Embelleciendo..."
},
"suggestions": {
"title": "Prueba a seguir preguntando:"
},
"supervisor": {
"todoList": {
"allComplete": "Todas las tareas están completadas",
+25
View File
@@ -2,7 +2,27 @@
"about": {
"title": "Acerca de"
},
"agent": {
"autoSuggestion": {
"customPrompt": {
"desc": "Guía a la IA sobre qué tipo de preguntas generar",
"placeholder": "Por ejemplo: centrarse en los detalles técnicos de implementación...",
"title": "Indicaciones personalizadas"
},
"enabled": {
"desc": "Generar automáticamente sugerencias de preguntas de seguimiento después de la respuesta del asistente",
"title": "Activar sugerencias automáticas"
},
"maxSuggestions": {
"desc": "Número de sugerencias a mostrar (1-3)",
"title": "Cantidad de sugerencias"
},
"submit": "Actualizar configuración",
"title": "Sugerencias automáticas"
}
},
"agentTab": {
"autoSuggestion": "Sugerencias automáticas",
"chat": "Preferencias de chat",
"meta": "Información del asistente",
"modal": "Configuración del modelo",
@@ -616,6 +636,11 @@
"modelDesc": "Modelo designado para generar el nombre, descripción, avatar y etiquetas del asistente",
"title": "Generación automática de información del asistente"
},
"autoSuggestion": {
"label": "Modelo de sugerencias inteligentes",
"modelDesc": "Especifica el modelo utilizado para generar sugerencias inteligentes de mensajes",
"title": "Sugerencias inteligentes de mensajes"
},
"customPrompt": {
"addPrompt": "Agregar aviso personalizado",
"desc": "Al completarlo, el asistente del sistema utilizará el aviso personalizado al generar contenido",
+7
View File
@@ -14,6 +14,10 @@
"thought": "فرآیند تفکر",
"unknownTitle": "اثر بدون نام"
},
"autoSuggestions": {
"generating": "در حال تولید پیشنهادها...",
"title": "پرسش‌های پیشنهادی"
},
"availableAgents": "دستیاران در دسترس",
"backToBottom": "بازگشت به پایین",
"chatList": {
@@ -323,6 +327,9 @@
"loading": "در حال شناسایی...",
"prettifying": "در حال ویرایش..."
},
"suggestions": {
"title": "سعی کنید بپرسید:"
},
"supervisor": {
"todoList": {
"allComplete": "همه وظایف انجام شده‌اند",
+25
View File
@@ -2,7 +2,27 @@
"about": {
"title": "درباره"
},
"agent": {
"autoSuggestion": {
"customPrompt": {
"desc": "راهنمایی به هوش مصنوعی برای تولید چه نوع سؤالاتی",
"placeholder": "مثلاً: تمرکز بر جزئیات فنی پیاده‌سازی...",
"title": "پیشنهاد سفارشی"
},
"enabled": {
"desc": "تولید خودکار پیشنهادهای سؤالات بعدی پس از پاسخ دستیار",
"title": "فعال‌سازی پیشنهاد خودکار"
},
"maxSuggestions": {
"desc": "تعداد پیشنهادهای نمایش داده شده (۱ تا ۳)",
"title": "تعداد پیشنهادها"
},
"submit": "به‌روزرسانی تنظیمات",
"title": "پیشنهاد خودکار"
}
},
"agentTab": {
"autoSuggestion": "پیشنهاد خودکار",
"chat": "ترجیحات گفتگو",
"meta": "اطلاعات دستیار",
"modal": "تنظیمات مدل",
@@ -616,6 +636,11 @@
"modelDesc": "مدلی که برای تولید نام، توضیحات، آواتار و برچسب‌های دستیار استفاده می‌شود",
"title": "تولید خودکار اطلاعات دستیار"
},
"autoSuggestion": {
"label": "مدل پیشنهاد هوشمند",
"modelDesc": "مدلی که برای تولید پیشنهادهای هوشمند پیام استفاده می‌شود را مشخص کنید",
"title": "پیشنهاد هوشمند پیام"
},
"customPrompt": {
"addPrompt": "افزودن اعلان سفارشی",
"desc": "پس از پر کردن، دستیار سیستم از اعلان سفارشی برای تولید محتوا استفاده خواهد کرد",
+7
View File
@@ -14,6 +14,10 @@
"thought": "Processus de pensée",
"unknownTitle": "Œuvre sans nom"
},
"autoSuggestions": {
"generating": "Génération de suggestions en cours...",
"title": "Questions suggérées"
},
"availableAgents": "Assistants disponibles",
"backToBottom": "Retour en bas",
"chatList": {
@@ -323,6 +327,9 @@
"loading": "En cours de reconnaissance...",
"prettifying": "En cours d'embellissement..."
},
"suggestions": {
"title": "Essayez de demander aussi :"
},
"supervisor": {
"todoList": {
"allComplete": "Toutes les tâches sont terminées",
+25
View File
@@ -2,7 +2,27 @@
"about": {
"title": "À propos"
},
"agent": {
"autoSuggestion": {
"customPrompt": {
"desc": "Guider l'IA sur le type de questions à générer",
"placeholder": "Par exemple : se concentrer sur les détails techniques de mise en œuvre...",
"title": "Invite personnalisée"
},
"enabled": {
"desc": "Générer automatiquement des suggestions de questions après la réponse de l'assistant",
"title": "Activer les suggestions automatiques"
},
"maxSuggestions": {
"desc": "Nombre de suggestions à afficher (1-3)",
"title": "Nombre de suggestions"
},
"submit": "Mettre à jour les paramètres",
"title": "Suggestions automatiques"
}
},
"agentTab": {
"autoSuggestion": "Suggestions automatiques",
"chat": "Préférences de discussion",
"meta": "Informations de l'agent",
"modal": "Paramètres du modèle",
@@ -616,6 +636,11 @@
"modelDesc": "Modèle spécifié pour générer le nom, la description, l'avatar et les balises de l'assistant",
"title": "Génération automatique des informations de l'assistant"
},
"autoSuggestion": {
"label": "Modèle de suggestions intelligentes",
"modelDesc": "Spécifie le modèle utilisé pour générer des suggestions de messages intelligents",
"title": "Suggestions de messages intelligents"
},
"customPrompt": {
"addPrompt": "Ajouter un prompt personnalisé",
"desc": "Une fois rempli, l'assistant système utilisera le prompt personnalisé lors de la génération de contenu",
+7
View File
@@ -14,6 +14,10 @@
"thought": "Processo di pensiero",
"unknownTitle": "Opera non nominata"
},
"autoSuggestions": {
"generating": "Generazione dei suggerimenti in corso...",
"title": "Domande suggerite"
},
"availableAgents": "Assistenti disponibili",
"backToBottom": "Torna in fondo",
"chatList": {
@@ -323,6 +327,9 @@
"loading": "Riconoscimento in corso...",
"prettifying": "Miglioramento in corso..."
},
"suggestions": {
"title": "Prova a chiedere anche:"
},
"supervisor": {
"todoList": {
"allComplete": "Tutti i compiti sono stati completati",
+25
View File
@@ -2,7 +2,27 @@
"about": {
"title": "Informazioni"
},
"agent": {
"autoSuggestion": {
"customPrompt": {
"desc": "Guida l'IA nel generare il tipo di domande desiderato",
"placeholder": "Ad esempio: concentrarsi sui dettagli tecnici dell'implementazione...",
"title": "Prompt personalizzato"
},
"enabled": {
"desc": "Genera automaticamente suggerimenti per domande successive dopo la risposta dell'assistente",
"title": "Abilita suggerimenti automatici"
},
"maxSuggestions": {
"desc": "Numero di suggerimenti da mostrare (1-3)",
"title": "Numero di suggerimenti"
},
"submit": "Aggiorna impostazioni",
"title": "Suggerimenti automatici"
}
},
"agentTab": {
"autoSuggestion": "Suggerimenti automatici",
"chat": "Preferenze di chat",
"meta": "Informazioni assistente",
"modal": "Impostazioni modello",
@@ -616,6 +636,11 @@
"modelDesc": "Modello specificato per generare nome, descrizione, avatar e etichetta dell'assistente",
"title": "Genera automaticamente informazioni sull'assistente"
},
"autoSuggestion": {
"label": "Modello di suggerimenti intelligenti",
"modelDesc": "Specifica il modello utilizzato per generare suggerimenti intelligenti nei messaggi",
"title": "Suggerimenti intelligenti per i messaggi"
},
"customPrompt": {
"addPrompt": "Aggiungi suggerimento personalizzato",
"desc": "Una volta compilato, l'assistente di sistema utilizzerà il suggerimento personalizzato nella generazione dei contenuti",
+7
View File
@@ -14,6 +14,10 @@
"thought": "思考過程",
"unknownTitle": "未命名の作品"
},
"autoSuggestions": {
"generating": "提案を生成中...",
"title": "提案された質問"
},
"availableAgents": "利用可能なアシスタント",
"backToBottom": "現在に戻る",
"chatList": {
@@ -323,6 +327,9 @@
"loading": "認識中...",
"prettifying": "整形中..."
},
"suggestions": {
"title": "こんな質問もいかがですか:"
},
"supervisor": {
"todoList": {
"allComplete": "すべてのタスクが完了しました",
+25
View File
@@ -2,7 +2,27 @@
"about": {
"title": "について"
},
"agent": {
"autoSuggestion": {
"customPrompt": {
"desc": "AI にどのような質問を生成させるかを指示します",
"placeholder": "例:技術的な実装の詳細に重点を置く...",
"title": "カスタムプロンプト"
},
"enabled": {
"desc": "アシスタントの返信後に自動で次の質問候補を生成します",
"title": "自動提案を有効にする"
},
"maxSuggestions": {
"desc": "表示する提案の数(13",
"title": "提案数"
},
"submit": "設定を更新",
"title": "自動提案"
}
},
"agentTab": {
"autoSuggestion": "自動提案",
"chat": "チャット設定",
"meta": "メタ情報",
"modal": "モーダル設定",
@@ -616,6 +636,11 @@
"modelDesc": "アシスタントの名前、説明、アバター、ラベルを生成するために指定されたモデル",
"title": "アシスタント情報の自動生成"
},
"autoSuggestion": {
"label": "インテリジェント提案モデル",
"modelDesc": "メッセージのインテリジェント提案を生成するためのモデルを指定します",
"title": "メッセージインテリジェント提案"
},
"customPrompt": {
"addPrompt": "カスタムプロンプトを追加",
"desc": "入力後、システムアシスタントは生成するコンテンツにカスタムプロンプトを使用します",
+7
View File
@@ -14,6 +14,10 @@
"thought": "사고 과정",
"unknownTitle": "제목 없음"
},
"autoSuggestions": {
"generating": "추천 생성 중...",
"title": "추천 질문"
},
"availableAgents": "사용 가능한 도우미",
"backToBottom": "하단으로 이동",
"chatList": {
@@ -323,6 +327,9 @@
"loading": "인식 중...",
"prettifying": "정제 중..."
},
"suggestions": {
"title": "다음 질문을 시도해 보세요:"
},
"supervisor": {
"todoList": {
"allComplete": "모든 작업이 완료되었습니다",
+25
View File
@@ -2,7 +2,27 @@
"about": {
"title": "정보"
},
"agent": {
"autoSuggestion": {
"customPrompt": {
"desc": "AI가 어떤 유형의 질문을 생성할지 안내합니다",
"placeholder": "예: 기술 구현 세부사항에 중점을 둠...",
"title": "사용자 정의 프롬프트"
},
"enabled": {
"desc": "도우미가 응답한 후 후속 질문 제안을 자동으로 생성합니다",
"title": "자동 제안 활성화"
},
"maxSuggestions": {
"desc": "표시할 제안 수 (1-3)",
"title": "제안 수"
},
"submit": "설정 업데이트",
"title": "자동 제안"
}
},
"agentTab": {
"autoSuggestion": "자동 제안",
"chat": "채팅 환경설정",
"meta": "도우미 정보",
"modal": "모델 설정",
@@ -616,6 +636,11 @@
"modelDesc": "어시스턴트 이름, 설명, 프로필 이미지, 레이블을 생성하는 데 사용되는 모델을 지정합니다.",
"title": "어시스턴트 정보 자동 생성"
},
"autoSuggestion": {
"label": "스마트 제안 모델",
"modelDesc": "메시지 스마트 제안을 생성하는 데 사용할 모델을 지정합니다",
"title": "메시지 스마트 제안"
},
"customPrompt": {
"addPrompt": "사용자 정의 프롬프트 추가",
"desc": "작성 후, 시스템 어시스턴트는 콘텐츠 생성 시 사용자 정의 프롬프트를 사용합니다.",
+7
View File
@@ -14,6 +14,10 @@
"thought": "Denken proces",
"unknownTitle": "Onbenoemd werk"
},
"autoSuggestions": {
"generating": "Suggesties worden gegenereerd...",
"title": "Voorgestelde vragen"
},
"availableAgents": "Beschikbare assistenten",
"backToBottom": "Terug naar onderen",
"chatList": {
@@ -323,6 +327,9 @@
"loading": "Bezig met herkennen...",
"prettifying": "Aan het verfraaien..."
},
"suggestions": {
"title": "Probeer verder te vragen:"
},
"supervisor": {
"todoList": {
"allComplete": "Alle taken zijn voltooid",
+25
View File
@@ -2,7 +2,27 @@
"about": {
"title": "Over"
},
"agent": {
"autoSuggestion": {
"customPrompt": {
"desc": "Geef AI richtlijnen over welk type vragen gegenereerd moeten worden",
"placeholder": "Bijvoorbeeld: focus op technische implementatiedetails...",
"title": "Aangepaste prompt"
},
"enabled": {
"desc": "Genereer automatisch vervolgvragen na het antwoord van de assistent",
"title": "Automatische suggesties inschakelen"
},
"maxSuggestions": {
"desc": "Aantal weergegeven suggesties (1-3)",
"title": "Aantal suggesties"
},
"submit": "Instellingen bijwerken",
"title": "Automatische suggesties"
}
},
"agentTab": {
"autoSuggestion": "Automatische suggesties",
"chat": "Chatvoorkeur",
"meta": "Assistentinformatie",
"modal": "Modelinstellingen",
@@ -616,6 +636,11 @@
"modelDesc": "Model voor het genereren van assistentnaam, beschrijving, profielfoto en labels",
"title": "Automatisch assistentinformatie genereren"
},
"autoSuggestion": {
"label": "Slim suggestiemodel",
"modelDesc": "Specificeer het model dat gebruikt wordt voor het genereren van slimme berichtsuggesties",
"title": "Slimme berichtsuggesties"
},
"customPrompt": {
"addPrompt": "Voeg aangepaste prompt toe",
"desc": "Vul dit in, zodat de systeemassistent de aangepaste prompt gebruikt bij het genereren van inhoud",
+7
View File
@@ -14,6 +14,10 @@
"thought": "Proces myślenia",
"unknownTitle": "Nienazwane dzieło"
},
"autoSuggestions": {
"generating": "Generowanie sugestii...",
"title": "Sugerowane pytania"
},
"availableAgents": "Dostępni asystenci",
"backToBottom": "Przewiń na dół",
"chatList": {
@@ -323,6 +327,9 @@
"loading": "Rozpoznawanie...",
"prettifying": "Upiększanie..."
},
"suggestions": {
"title": "Spróbuj zapytać dalej:"
},
"supervisor": {
"todoList": {
"allComplete": "Wszystkie zadania zostały ukończone",
+25
View File
@@ -2,7 +2,27 @@
"about": {
"title": "O nas"
},
"agent": {
"autoSuggestion": {
"customPrompt": {
"desc": "Wskazówki dla AI, jaki typ pytań generować",
"placeholder": "Na przykład: Skoncentruj się na szczegółach technicznej implementacji...",
"title": "Własna podpowiedź"
},
"enabled": {
"desc": "Automatycznie generuj sugestie kolejnych pytań po odpowiedzi asystenta",
"title": "Włącz automatyczne sugestie"
},
"maxSuggestions": {
"desc": "Liczba wyświetlanych sugestii (13)",
"title": "Liczba sugestii"
},
"submit": "Zaktualizuj ustawienia",
"title": "Automatyczne sugestie"
}
},
"agentTab": {
"autoSuggestion": "Automatyczne sugestie",
"chat": "Preferencje czatu",
"meta": "Informacje o asystencie",
"modal": "Ustawienia modalne",
@@ -616,6 +636,11 @@
"modelDesc": "Określa model używany do generowania nazwy, opisu, awatara i etykiety asystenta",
"title": "Automatyczne generowanie informacji o asystencie"
},
"autoSuggestion": {
"label": "Model inteligentnych sugestii",
"modelDesc": "Określ model używany do generowania inteligentnych sugestii wiadomości",
"title": "Inteligentne sugestie wiadomości"
},
"customPrompt": {
"addPrompt": "Dodaj niestandardowy podpowiedź",
"desc": "Po wypełnieniu, asystent systemowy użyje niestandardowej podpowiedzi podczas generowania treści",
+7
View File
@@ -14,6 +14,10 @@
"thought": "Processo de pensamento",
"unknownTitle": "Obra sem título"
},
"autoSuggestions": {
"generating": "Gerando sugestões...",
"title": "Perguntas sugeridas"
},
"availableAgents": "Assistentes disponíveis",
"backToBottom": "Voltar para o início",
"chatList": {
@@ -323,6 +327,9 @@
"loading": "Reconhecendo...",
"prettifying": "Embelezando..."
},
"suggestions": {
"title": "Tente perguntar também:"
},
"supervisor": {
"todoList": {
"allComplete": "Todas as tarefas foram concluídas",
+25
View File
@@ -2,7 +2,27 @@
"about": {
"title": "Sobre"
},
"agent": {
"autoSuggestion": {
"customPrompt": {
"desc": "Orientar a IA sobre o tipo de pergunta a ser gerada",
"placeholder": "Por exemplo: focar nos detalhes técnicos da implementação...",
"title": "Prompt personalizado"
},
"enabled": {
"desc": "Gerar automaticamente sugestões de perguntas após a resposta do assistente",
"title": "Ativar sugestões automáticas"
},
"maxSuggestions": {
"desc": "Número de sugestões exibidas (1-3)",
"title": "Quantidade de sugestões"
},
"submit": "Atualizar configurações",
"title": "Sugestões automáticas"
}
},
"agentTab": {
"autoSuggestion": "Sugestões automáticas",
"chat": "Preferências de bate-papo",
"meta": "Informações do assistente",
"modal": "Configurações do modelo",
@@ -616,6 +636,11 @@
"modelDesc": "Especifica o modelo usado para gerar o nome, descrição, avatar e tags do assistente",
"title": "Geração Automática de Informações do Assistente"
},
"autoSuggestion": {
"label": "Modelo de sugestões inteligentes",
"modelDesc": "Especificar o modelo usado para gerar sugestões inteligentes de mensagens",
"title": "Sugestões inteligentes de mensagens"
},
"customPrompt": {
"addPrompt": "Adicionar prompt personalizado",
"desc": "Após preenchido, o assistente do sistema usará o prompt personalizado ao gerar conteúdo",
+7
View File
@@ -14,6 +14,10 @@
"thought": "Процесс мышления",
"unknownTitle": "Безымянное произведение"
},
"autoSuggestions": {
"generating": "Генерация предложений...",
"title": "Рекомендуемые вопросы"
},
"availableAgents": "Доступные помощники",
"backToBottom": "Вернуться вниз",
"chatList": {
@@ -323,6 +327,9 @@
"loading": "Распознавание...",
"prettifying": "Форматирование..."
},
"suggestions": {
"title": "Попробуйте спросить ещё:"
},
"supervisor": {
"todoList": {
"allComplete": "Все задачи выполнены",
+25
View File
@@ -2,7 +2,27 @@
"about": {
"title": "О нас"
},
"agent": {
"autoSuggestion": {
"customPrompt": {
"desc": "Направляйте ИИ, какой тип вопросов генерировать",
"placeholder": "Например: сосредоточьтесь на технических деталях реализации...",
"title": "Пользовательская подсказка"
},
"enabled": {
"desc": "Автоматически предлагать последующие вопросы после ответа помощника",
"title": "Включить авто-подсказки"
},
"maxSuggestions": {
"desc": "Количество отображаемых предложений (1–3)",
"title": "Количество предложений"
},
"submit": "Обновить настройки",
"title": "Автоматические подсказки"
}
},
"agentTab": {
"autoSuggestion": "Автоматические подсказки",
"chat": "Предпочтения чата",
"meta": "Информация об ассистенте",
"modal": "Настройки модели",
@@ -616,6 +636,11 @@
"modelDesc": "Модель, используемая для генерации имени агента, описания, аватара и меток",
"title": "Автоматическое создание информации об агенте"
},
"autoSuggestion": {
"label": "Модель интеллектуальных подсказок",
"modelDesc": "Укажите модель, используемую для генерации интеллектуальных предложений сообщений",
"title": "Интеллектуальные предложения сообщений"
},
"customPrompt": {
"addPrompt": "Добавить пользовательский запрос",
"desc": "После заполнения система будет использовать пользовательский запрос при генерации контента",
+7
View File
@@ -14,6 +14,10 @@
"thought": "Düşünce Süreci",
"unknownTitle": "İsimsiz Eser"
},
"autoSuggestions": {
"generating": "Öneriler oluşturuluyor...",
"title": "Önerilen Sorular"
},
"availableAgents": "Kullanılabilir asistanlar",
"backToBottom": "En alta git",
"chatList": {
@@ -323,6 +327,9 @@
"loading": "Tanımlanıyor...",
"prettifying": "İyileştiriliyor..."
},
"suggestions": {
"title": "Şunları sormayı deneyin:"
},
"supervisor": {
"todoList": {
"allComplete": "Tüm görevler tamamlandı",
+25
View File
@@ -2,7 +2,27 @@
"about": {
"title": "Hakkında"
},
"agent": {
"autoSuggestion": {
"customPrompt": {
"desc": "Yapay zekanın ne tür sorular oluşturacağını yönlendirin",
"placeholder": "Örneğin: Teknik uygulama detaylarına odaklan...",
"title": "Özel İpucu"
},
"enabled": {
"desc": "Asistan yanıt verdikten sonra otomatik olarak takip soruları öner",
"title": "Otomatik Öneriyi Etkinleştir"
},
"maxSuggestions": {
"desc": "Gösterilecek öneri sayısı (1-3)",
"title": "Öneri Sayısı"
},
"submit": "Ayarları Güncelle",
"title": "Otomatik Öneri"
}
},
"agentTab": {
"autoSuggestion": "Otomatik Öneri",
"chat": "Sohbet Tercihi",
"meta": "Asistan Bilgisi",
"modal": "Model Ayarları",
@@ -616,6 +636,11 @@
"modelDesc": "Asistan adı, açıklaması, avatar ve etiket oluşturmak için belirlenen model",
"title": "Asistan Bilgilerini Otomatik Oluştur"
},
"autoSuggestion": {
"label": "Akıllı Öneri Modeli",
"modelDesc": "Mesaj önerileri oluşturmak için kullanılacak modeli belirtin",
"title": "Mesaj Akıllı Önerisi"
},
"customPrompt": {
"addPrompt": "Özel İpucu Ekle",
"desc": "Doldurduğunuzda, sistem asistanı içerik oluştururken özel ipucunu kullanacaktır",
+7
View File
@@ -14,6 +14,10 @@
"thought": "Quá trình suy nghĩ",
"unknownTitle": "Tác phẩm chưa được đặt tên"
},
"autoSuggestions": {
"generating": "Đang tạo đề xuất...",
"title": "Câu hỏi gợi ý"
},
"availableAgents": "Trợ lý có sẵn",
"backToBottom": "Quay về dưới cùng",
"chatList": {
@@ -323,6 +327,9 @@
"loading": "Đang nhận dạng...",
"prettifying": "Đang tinh chỉnh..."
},
"suggestions": {
"title": "Hãy thử hỏi tiếp:"
},
"supervisor": {
"todoList": {
"allComplete": "Tất cả nhiệm vụ đã hoàn thành",
+25
View File
@@ -2,7 +2,27 @@
"about": {
"title": "Về chúng tôi"
},
"agent": {
"autoSuggestion": {
"customPrompt": {
"desc": "Hướng dẫn AI tạo ra loại câu hỏi nào",
"placeholder": "Ví dụ: Tập trung vào chi tiết kỹ thuật...",
"title": "Tùy chỉnh lời nhắc"
},
"enabled": {
"desc": "Tự động tạo đề xuất câu hỏi tiếp theo sau khi trợ lý trả lời",
"title": "Bật đề xuất tự động"
},
"maxSuggestions": {
"desc": "Số lượng đề xuất hiển thị (1-3)",
"title": "Số lượng đề xuất"
},
"submit": "Cập nhật cài đặt",
"title": "Đề xuất tự động"
}
},
"agentTab": {
"autoSuggestion": "Đề xuất tự động",
"chat": "Tùy chọn Trò chuyện",
"meta": "Thông tin Trợ lý",
"modal": "Cài đặt Mô hình",
@@ -616,6 +636,11 @@
"modelDesc": "Xác định mô hình được sử dụng để tạo tên, mô tả, hình đại diện, nhãn cho trợ lý",
"title": "Tự động tạo thông tin trợ lý"
},
"autoSuggestion": {
"label": "Mô hình đề xuất thông minh",
"modelDesc": "Chỉ định mô hình dùng để tạo đề xuất tin nhắn thông minh",
"title": "Đề xuất tin nhắn thông minh"
},
"customPrompt": {
"addPrompt": "Thêm gợi ý tùy chỉnh",
"desc": "Sau khi điền, trợ lý hệ thống sẽ sử dụng gợi ý tùy chỉnh khi tạo nội dung",
+7
View File
@@ -14,6 +14,10 @@
"thought": "思考过程",
"unknownTitle": "未命名作品"
},
"autoSuggestions": {
"generating": "生成建议中...",
"title": "建议问题"
},
"availableAgents": "可用助手",
"backToBottom": "跳转至当前",
"chatList": {
@@ -323,6 +327,9 @@
"loading": "识别中...",
"prettifying": "润色中..."
},
"suggestions": {
"title": "试试继续问:"
},
"supervisor": {
"todoList": {
"allComplete": "所有任务已完成",
+25
View File
@@ -2,7 +2,27 @@
"about": {
"title": "关于"
},
"agent": {
"autoSuggestion": {
"customPrompt": {
"desc": "指导 AI 生成什么类型的问题",
"placeholder": "例如:侧重于技术实现细节...",
"title": "自定义提示词"
},
"enabled": {
"desc": "在助手回复后自动生成后续问题建议",
"title": "启用自动建议"
},
"maxSuggestions": {
"desc": "显示建议的数量(1-3",
"title": "建议数量"
},
"submit": "更新设置",
"title": "自动建议"
}
},
"agentTab": {
"autoSuggestion": "自动建议",
"chat": "聊天偏好",
"meta": "助手信息",
"modal": "模型设置",
@@ -616,6 +636,11 @@
"modelDesc": "指定用于生成助理名称、描述、头像、标签的模型",
"title": "自动生成助理信息"
},
"autoSuggestion": {
"label": "智能建议模型",
"modelDesc": "指定用于生成消息智能建议的模型",
"title": "消息智能建议"
},
"customPrompt": {
"addPrompt": "添加自定义提示",
"desc": "填写后,系统助理将在生成内容时使用自定义提示",
+7
View File
@@ -14,6 +14,10 @@
"thought": "思考過程",
"unknownTitle": "未命名作品"
},
"autoSuggestions": {
"generating": "正在產生建議...",
"title": "建議問題"
},
"availableAgents": "可用助理",
"backToBottom": "返回底部",
"chatList": {
@@ -323,6 +327,9 @@
"loading": "識別中...",
"prettifying": "潤色中..."
},
"suggestions": {
"title": "試著繼續問:"
},
"supervisor": {
"todoList": {
"allComplete": "所有任務已完成",
+25
View File
@@ -2,7 +2,27 @@
"about": {
"title": "關於"
},
"agent": {
"autoSuggestion": {
"customPrompt": {
"desc": "引導 AI 生成什麼類型的問題",
"placeholder": "例如:著重於技術實作細節...",
"title": "自訂提示詞"
},
"enabled": {
"desc": "在助手回覆後自動產生後續問題建議",
"title": "啟用自動建議"
},
"maxSuggestions": {
"desc": "顯示建議的數量(1-3",
"title": "建議數量"
},
"submit": "更新設定",
"title": "自動建議"
}
},
"agentTab": {
"autoSuggestion": "自動建議",
"chat": "聊天偏好",
"meta": "助手資訊",
"modal": "模型設定",
@@ -616,6 +636,11 @@
"modelDesc": "指定用於生成助理名稱、描述、頭像、標籤的模型",
"title": "自動生成助理資訊"
},
"autoSuggestion": {
"label": "智慧建議模型",
"modelDesc": "指定用於產生訊息智慧建議的模型",
"title": "訊息智慧建議"
},
"customPrompt": {
"addPrompt": "新增自訂提示",
"desc": "填寫後,系統助理將在生成內容時使用自訂提示",
+2 -2
View File
@@ -1,4 +1,4 @@
// in 2025.01.26
export const USD_TO_CNY = 7.24;
// in 2025.10.18
export const USD_TO_CNY = 7.13;
export const CREDITS_PER_DOLLAR = 1_000_000;
+7
View File
@@ -2,6 +2,7 @@ import {
LobeAgentChatConfig,
LobeAgentConfig,
LobeAgentTTSConfig,
LobeAutoSuggestion,
UserDefaultAgent,
} from '@lobechat/types';
@@ -22,8 +23,14 @@ export const DEFAULT_AGENT_SEARCH_FC_MODEL = {
provider: DEFAULT_PROVIDER,
};
export const DEFAULT_AUTO_SUGGESTION: LobeAutoSuggestion = {
enabled: true,
maxSuggestions: 3,
};
export const DEFAULT_AGENT_CHAT_CONFIG: LobeAgentChatConfig = {
autoCreateTopicThreshold: 2,
autoSuggestion: DEFAULT_AUTO_SUGGESTION,
displayMode: 'chat',
enableAutoCreateTopic: true,
enableCompressHistory: true,
@@ -15,6 +15,7 @@ export const DEFAULT_QUERY_REWRITE_SYSTEM_AGENT_ITEM: QueryRewriteSystemAgent =
export const DEFAULT_SYSTEM_AGENT_CONFIG: UserSystemAgentConfig = {
agentMeta: DEFAULT_SYSTEM_AGENT_ITEM,
autoSuggestion: { ...DEFAULT_SYSTEM_AGENT_ITEM, model: 'gpt-4.1-nano' },
generationTopic: DEFAULT_SYSTEM_AGENT_ITEM,
groupChatSupervisor: DEFAULT_SYSTEM_AGENT_ITEM,
historyCompress: DEFAULT_SYSTEM_AGENT_ITEM,
+2 -2
View File
@@ -71,7 +71,7 @@ export class ContextEngine {
* Execute pipeline processing
*/
async process(input: {
initialState: AgentState;
initialState?: AgentState;
maxTokens: number;
messages?: Array<any>;
metadata?: Record<string, any>;
@@ -82,7 +82,7 @@ export class ContextEngine {
// Create initial pipeline context
let context: PipelineContext = {
initialState: input.initialState,
initialState: input.initialState!,
isAborted: false,
messages: Array.isArray(input.messages) ? [...input.messages] : [],
metadata: {
@@ -7,10 +7,10 @@ import { uuid } from '@/utils/uuid';
import { getTestDB } from '../../models/__tests__/_util';
import {
chunks,
embeddings,
agents,
chatGroups,
chunks,
embeddings,
fileChunks,
files,
messagePlugins,
@@ -1290,6 +1290,204 @@ describe('MessageModel', () => {
});
});
describe('updateMetadata', () => {
it('should update metadata for an existing message', async () => {
// 创建测试数据
await serverDB.insert(messages).values({
id: 'msg-with-metadata',
userId,
role: 'user',
content: 'test message',
metadata: { existingKey: 'existingValue' },
});
// 调用 updateMetadata 方法
await messageModel.updateMetadata('msg-with-metadata', { newKey: 'newValue' });
// 断言结果
const result = await serverDB
.select()
.from(messages)
.where(eq(messages.id, 'msg-with-metadata'));
expect(result[0].metadata).toEqual({
existingKey: 'existingValue',
newKey: 'newValue',
});
});
it('should merge new metadata with existing metadata using lodash merge behavior', async () => {
// 创建测试数据
await serverDB.insert(messages).values({
id: 'msg-merge-metadata',
userId,
role: 'assistant',
content: 'test message',
metadata: {
level1: {
level2a: 'original',
level2b: { level3: 'deep' },
},
array: [1, 2, 3],
},
});
// 调用 updateMetadata 方法
await messageModel.updateMetadata('msg-merge-metadata', {
level1: {
level2a: 'updated',
level2c: 'new',
},
newTopLevel: 'value',
});
// 断言结果 - 应该使用 lodash merge 行为
const result = await serverDB
.select()
.from(messages)
.where(eq(messages.id, 'msg-merge-metadata'));
expect(result[0].metadata).toEqual({
level1: {
level2a: 'updated',
level2b: { level3: 'deep' },
level2c: 'new',
},
array: [1, 2, 3],
newTopLevel: 'value',
});
});
it('should handle non-existent message IDs', async () => {
// 调用 updateMetadata 方法,尝试更新不存在的消息
const result = await messageModel.updateMetadata('non-existent-id', { key: 'value' });
// 断言结果 - 应该返回 undefined
expect(result).toBeUndefined();
});
it('should handle empty metadata updates', async () => {
// 创建测试数据
await serverDB.insert(messages).values({
id: 'msg-empty-metadata',
userId,
role: 'user',
content: 'test message',
metadata: { originalKey: 'originalValue' },
});
// 调用 updateMetadata 方法,传递空对象
await messageModel.updateMetadata('msg-empty-metadata', {});
// 断言结果 - 原始 metadata 应该保持不变
const result = await serverDB
.select()
.from(messages)
.where(eq(messages.id, 'msg-empty-metadata'));
expect(result[0].metadata).toEqual({ originalKey: 'originalValue' });
});
it('should handle message with null metadata', async () => {
// 创建测试数据
await serverDB.insert(messages).values({
id: 'msg-null-metadata',
userId,
role: 'user',
content: 'test message',
metadata: null,
});
// 调用 updateMetadata 方法
await messageModel.updateMetadata('msg-null-metadata', { key: 'value' });
// 断言结果 - 应该创建新的 metadata
const result = await serverDB
.select()
.from(messages)
.where(eq(messages.id, 'msg-null-metadata'));
expect(result[0].metadata).toEqual({ key: 'value' });
});
it('should only update messages belonging to the current user', async () => {
// 创建测试数据 - 其他用户的消息
await serverDB.insert(messages).values({
id: 'msg-other-user',
userId: '456',
role: 'user',
content: 'test message',
metadata: { originalKey: 'originalValue' },
});
// 调用 updateMetadata 方法
const result = await messageModel.updateMetadata('msg-other-user', {
hackedKey: 'hackedValue',
});
// 断言结果 - 应该返回 undefined
expect(result).toBeUndefined();
// 验证原始 metadata 未被修改
const dbResult = await serverDB
.select()
.from(messages)
.where(eq(messages.id, 'msg-other-user'));
expect(dbResult[0].metadata).toEqual({ originalKey: 'originalValue' });
});
it('should handle complex nested metadata updates', async () => {
// 创建测试数据
await serverDB.insert(messages).values({
id: 'msg-complex-metadata',
userId,
role: 'assistant',
content: 'test message',
metadata: {
config: {
settings: {
enabled: true,
options: ['a', 'b'],
},
version: 1,
},
},
});
// 调用 updateMetadata 方法
await messageModel.updateMetadata('msg-complex-metadata', {
config: {
settings: {
enabled: false,
timeout: 5000,
},
newField: 'value',
},
stats: { count: 10 },
});
// 断言结果
const result = await serverDB
.select()
.from(messages)
.where(eq(messages.id, 'msg-complex-metadata'));
expect(result[0].metadata).toEqual({
config: {
settings: {
enabled: false,
options: ['a', 'b'],
timeout: 5000,
},
version: 1,
newField: 'value',
},
stats: { count: 10 },
});
});
});
describe('updateTranslate', () => {
it('should insert a new record if message does not exist in messageTranslates table', async () => {
// 创建测试数据
+13
View File
@@ -570,6 +570,19 @@ export class MessageModel {
});
};
updateMetadata = async (id: string, metadata: Record<string, any>) => {
const item = await this.db.query.messages.findFirst({
where: and(eq(messages.id, id), eq(messages.userId, this.userId)),
});
if (!item) return;
return this.db
.update(messages)
.set({ metadata: merge(item.metadata || {}, metadata) })
.where(and(eq(messages.userId, this.userId), eq(messages.id, id)));
};
updatePluginState = async (id: string, state: Record<string, any>) => {
const item = await this.db.query.messagePlugins.findFirst({
where: eq(messagePlugins.id, id),
@@ -2048,6 +2048,319 @@ describe('LobeOpenAICompatibleFactory', () => {
});
});
describe('handleSchema option', () => {
let instanceWithSchemaHandler: any;
const mockSchemaHandler = vi.fn((schema: any) => {
const filtered: any = {};
for (const [key, value] of Object.entries(schema)) {
if (key !== 'maxLength' && key !== 'pattern') {
filtered[key] = value;
}
}
return filtered;
});
beforeEach(() => {
mockSchemaHandler.mockClear();
const RuntimeClass = createOpenAICompatibleRuntime({
baseURL: 'https://api.test.com',
generateObject: {
handleSchema: mockSchemaHandler,
},
provider: 'test-provider',
});
instanceWithSchemaHandler = new RuntimeClass({ apiKey: 'test-key' });
});
it('should apply schema transformation with Responses API', async () => {
const mockResponse = {
output_text: '{"name":"Alice","age":30}',
};
vi.spyOn(instanceWithSchemaHandler['client'].responses, 'create').mockResolvedValue(
mockResponse as any,
);
const payload = {
messages: [{ content: 'Extract person', role: 'user' as const }],
model: 'gpt-4o',
responseApi: true,
schema: {
name: 'person',
schema: {
maxLength: 100,
pattern: '^[a-z]+$',
properties: {
age: { type: 'number' },
name: { type: 'string' },
},
type: 'object' as const,
},
},
};
await instanceWithSchemaHandler.generateObject(payload);
expect(mockSchemaHandler).toHaveBeenCalledWith(payload.schema.schema);
expect(instanceWithSchemaHandler['client'].responses.create).toHaveBeenCalledWith(
expect.objectContaining({
text: expect.objectContaining({
format: expect.objectContaining({
schema: {
properties: {
age: { type: 'number' },
name: { type: 'string' },
},
type: 'object',
},
}),
}),
}),
expect.any(Object),
);
});
it('should apply schema transformation with Chat Completions API', async () => {
const mockResponse = {
choices: [
{
message: {
content: '{"name":"Bob","age":25}',
},
},
],
};
vi.spyOn(instanceWithSchemaHandler['client'].chat.completions, 'create').mockResolvedValue(
mockResponse as any,
);
const payload = {
messages: [{ content: 'Extract person', role: 'user' as const }],
model: 'test-model',
schema: {
name: 'person',
schema: {
maxLength: 100,
pattern: '^[a-z]+$',
properties: {
age: { type: 'number' },
name: { type: 'string' },
},
type: 'object' as const,
},
},
};
await instanceWithSchemaHandler.generateObject(payload);
expect(mockSchemaHandler).toHaveBeenCalledWith(payload.schema.schema);
expect(instanceWithSchemaHandler['client'].chat.completions.create).toHaveBeenCalledWith(
expect.objectContaining({
response_format: expect.objectContaining({
json_schema: expect.objectContaining({
schema: {
properties: {
age: { type: 'number' },
name: { type: 'string' },
},
type: 'object',
},
}),
}),
}),
expect.any(Object),
);
});
it('should apply schema transformation with tool calling fallback', async () => {
const RuntimeClass = createOpenAICompatibleRuntime({
baseURL: 'https://api.test.com',
generateObject: {
handleSchema: mockSchemaHandler,
useToolsCalling: true,
},
provider: 'test-provider',
});
const instance = new RuntimeClass({ apiKey: 'test-key' });
const mockResponse = {
choices: [
{
message: {
tool_calls: [
{
function: {
arguments: '{"name":"Charlie","age":35}',
name: 'person',
},
type: 'function' as const,
},
],
},
},
],
};
vi.spyOn(instance['client'].chat.completions, 'create').mockResolvedValue(
mockResponse as any,
);
const payload = {
messages: [{ content: 'Extract person', role: 'user' as const }],
model: 'test-model',
schema: {
name: 'person',
schema: {
maxLength: 100,
pattern: '^[a-z]+$',
properties: {
age: { type: 'number' },
name: { type: 'string' },
},
type: 'object' as const,
},
},
};
await instance.generateObject(payload);
expect(mockSchemaHandler).toHaveBeenCalledWith(payload.schema.schema);
expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
expect.objectContaining({
tools: [
expect.objectContaining({
function: expect.objectContaining({
parameters: {
properties: {
age: { type: 'number' },
name: { type: 'string' },
},
type: 'object',
},
}),
}),
],
}),
expect.any(Object),
);
});
it('should not apply schema transformation when handleSchema is not configured', async () => {
const RuntimeClass = createOpenAICompatibleRuntime({
baseURL: 'https://api.test.com',
provider: 'test-provider',
});
const instance = new RuntimeClass({ apiKey: 'test-key' });
const mockResponse = {
choices: [
{
message: {
content: '{"name":"Test"}',
},
},
],
};
vi.spyOn(instance['client'].chat.completions, 'create').mockResolvedValue(
mockResponse as any,
);
const payload = {
messages: [{ content: 'Extract data', role: 'user' as const }],
model: 'test-model',
schema: {
name: 'test',
schema: {
maxLength: 100,
properties: {
name: { type: 'string' },
},
type: 'object' as const,
},
},
};
await instance.generateObject(payload);
expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
expect.objectContaining({
response_format: expect.objectContaining({
json_schema: expect.objectContaining({
schema: {
maxLength: 100,
properties: {
name: { type: 'string' },
},
type: 'object',
},
}),
}),
}),
expect.any(Object),
);
});
it('should preserve original schema properties while filtering', async () => {
const mockResponse = {
output_text: '{"result":"success"}',
};
vi.spyOn(instanceWithSchemaHandler['client'].responses, 'create').mockResolvedValue(
mockResponse as any,
);
const payload = {
messages: [{ content: 'Test', role: 'user' as const }],
model: 'gpt-4o',
responseApi: true,
schema: {
description: 'Test schema',
name: 'test',
schema: {
description: 'Inner schema description',
maxLength: 100,
pattern: '^test$',
properties: {
result: { type: 'string' },
},
required: ['result'],
type: 'object' as const,
},
strict: true,
},
};
await instanceWithSchemaHandler.generateObject(payload);
expect(mockSchemaHandler).toHaveBeenCalledWith(payload.schema.schema);
expect(instanceWithSchemaHandler['client'].responses.create).toHaveBeenCalledWith(
expect.objectContaining({
text: expect.objectContaining({
format: expect.objectContaining({
description: 'Test schema',
name: 'test',
schema: {
description: 'Inner schema description',
properties: {
result: { type: 'string' },
},
required: ['result'],
type: 'object',
},
strict: true,
}),
}),
}),
expect.any(Object),
);
});
});
describe('tool calling fallback', () => {
let instanceWithToolCalling: any;
@@ -119,6 +119,10 @@ export interface OpenAICompatibleFactoryOptions<T extends Record<string, any> =
invalidAPIKey: ILobeAgentRuntimeErrorType;
};
generateObject?: {
/**
* Transform schema before sending to the provider (e.g., filter unsupported properties)
*/
handleSchema?: (schema: any) => any;
/**
* If true, route generateObject requests to Responses API path directly
*/
@@ -454,12 +458,19 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
// Use tool calling fallback if configured
if (generateObjectConfig?.useToolsCalling) {
log('using tool calling fallback for structured output');
// Apply schema transformation if configured
const processedSchema = generateObjectConfig.handleSchema
? { ...schema, schema: generateObjectConfig.handleSchema(schema.schema) }
: schema;
const tool: ChatCompletionTool = {
function: {
description:
schema.description || 'Generate structured output according to the provided schema',
name: schema.name || 'structured_output',
parameters: schema.schema,
processedSchema.description ||
'Generate structured output according to the provided schema',
name: processedSchema.name || 'structured_output',
parameters: processedSchema.schema,
},
type: 'function',
};
@@ -531,13 +542,18 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
return false;
})();
// Apply schema transformation if configured
const processedSchema = generateObjectConfig?.handleSchema
? { ...schema, schema: generateObjectConfig.handleSchema(schema.schema) }
: schema;
if (shouldUseResponses) {
log('calling responses.create for structured output');
const res = await this.client!.responses.create(
{
input: messages,
model,
text: { format: { strict: true, type: 'json_schema', ...schema } },
text: { format: { strict: true, type: 'json_schema', ...processedSchema } },
user: options?.user,
},
{ headers: options?.headers, signal: options?.signal },
@@ -561,7 +577,7 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
{
messages,
model,
response_format: { json_schema: schema, type: 'json_schema' },
response_format: { json_schema: processedSchema, type: 'json_schema' },
user: options?.user,
},
{ headers: options?.headers, signal: options?.signal },
@@ -33,6 +33,455 @@ afterEach(() => {
});
describe('LobeGroq - custom features', () => {
describe('filterAdvancedFields', () => {
const filterAdvancedFields = params.generateObject!.handleSchema!;
it('should filter out maxItems from schema', () => {
const schema = {
items: { type: 'string' },
maxItems: 5,
type: 'array',
};
const result = filterAdvancedFields(schema);
expect(result).toEqual({
items: { type: 'string' },
type: 'array',
});
expect(result.maxItems).toBeUndefined();
});
it('should filter out minItems from schema', () => {
const schema = {
items: { type: 'string' },
minItems: 2,
type: 'array',
};
const result = filterAdvancedFields(schema);
expect(result).toEqual({
items: { type: 'string' },
type: 'array',
});
expect(result.minItems).toBeUndefined();
});
it('should filter out maxLength from schema', () => {
const schema = {
maxLength: 100,
type: 'string',
};
const result = filterAdvancedFields(schema);
expect(result).toEqual({
type: 'string',
});
expect(result.maxLength).toBeUndefined();
});
it('should filter out minLength from schema', () => {
const schema = {
minLength: 5,
type: 'string',
};
const result = filterAdvancedFields(schema);
expect(result).toEqual({
type: 'string',
});
expect(result.minLength).toBeUndefined();
});
it('should filter out pattern from schema', () => {
const schema = {
pattern: '^[a-z]+$',
type: 'string',
};
const result = filterAdvancedFields(schema);
expect(result).toEqual({
type: 'string',
});
expect(result.pattern).toBeUndefined();
});
it('should filter out format from schema', () => {
const schema = {
format: 'email',
type: 'string',
};
const result = filterAdvancedFields(schema);
expect(result).toEqual({
type: 'string',
});
expect(result.format).toBeUndefined();
});
it('should filter out uniqueItems from schema', () => {
const schema = {
items: { type: 'number' },
type: 'array',
uniqueItems: true,
};
const result = filterAdvancedFields(schema);
expect(result).toEqual({
items: { type: 'number' },
type: 'array',
});
expect(result.uniqueItems).toBeUndefined();
});
it('should filter out maxProperties from schema', () => {
const schema = {
maxProperties: 10,
type: 'object',
};
const result = filterAdvancedFields(schema);
expect(result).toEqual({
type: 'object',
});
expect(result.maxProperties).toBeUndefined();
});
it('should filter out minProperties from schema', () => {
const schema = {
minProperties: 2,
type: 'object',
};
const result = filterAdvancedFields(schema);
expect(result).toEqual({
type: 'object',
});
expect(result.minProperties).toBeUndefined();
});
it('should filter out multipleOf from schema', () => {
const schema = {
multipleOf: 5,
type: 'number',
};
const result = filterAdvancedFields(schema);
expect(result).toEqual({
type: 'number',
});
expect(result.multipleOf).toBeUndefined();
});
it('should filter out maximum from schema', () => {
const schema = {
maximum: 100,
type: 'number',
};
const result = filterAdvancedFields(schema);
expect(result).toEqual({
type: 'number',
});
expect(result.maximum).toBeUndefined();
});
it('should filter out minimum from schema', () => {
const schema = {
minimum: 0,
type: 'number',
};
const result = filterAdvancedFields(schema);
expect(result).toEqual({
type: 'number',
});
expect(result.minimum).toBeUndefined();
});
it('should filter out exclusiveMaximum from schema', () => {
const schema = {
exclusiveMaximum: 100,
type: 'number',
};
const result = filterAdvancedFields(schema);
expect(result).toEqual({
type: 'number',
});
expect(result.exclusiveMaximum).toBeUndefined();
});
it('should filter out exclusiveMinimum from schema', () => {
const schema = {
exclusiveMinimum: 0,
type: 'number',
};
const result = filterAdvancedFields(schema);
expect(result).toEqual({
type: 'number',
});
expect(result.exclusiveMinimum).toBeUndefined();
});
it('should filter out multiple unsupported properties at once', () => {
const schema = {
format: 'email',
maxLength: 100,
minLength: 5,
pattern: '^[a-z]+$',
type: 'string',
};
const result = filterAdvancedFields(schema);
expect(result).toEqual({
type: 'string',
});
expect(result.maxLength).toBeUndefined();
expect(result.minLength).toBeUndefined();
expect(result.pattern).toBeUndefined();
expect(result.format).toBeUndefined();
});
it('should preserve supported properties', () => {
const schema = {
description: 'A test field',
enum: ['a', 'b', 'c'],
maxLength: 10,
type: 'string',
};
const result = filterAdvancedFields(schema);
expect(result).toEqual({
description: 'A test field',
enum: ['a', 'b', 'c'],
type: 'string',
});
});
it('should handle nested objects recursively', () => {
const schema = {
properties: {
email: {
format: 'email',
maxLength: 100,
type: 'string',
},
name: {
minLength: 2,
type: 'string',
},
},
type: 'object',
};
const result = filterAdvancedFields(schema);
expect(result).toEqual({
properties: {
email: {
type: 'string',
},
name: {
type: 'string',
},
},
type: 'object',
});
});
it('should handle deeply nested objects', () => {
const schema = {
properties: {
user: {
properties: {
address: {
maxProperties: 5,
properties: {
city: {
maxLength: 50,
type: 'string',
},
zip: {
pattern: '^\\d{5}$',
type: 'string',
},
},
type: 'object',
},
name: {
minLength: 1,
type: 'string',
},
},
type: 'object',
},
},
type: 'object',
};
const result = filterAdvancedFields(schema);
expect(result).toEqual({
properties: {
user: {
properties: {
address: {
properties: {
city: {
type: 'string',
},
zip: {
type: 'string',
},
},
type: 'object',
},
name: {
type: 'string',
},
},
type: 'object',
},
},
type: 'object',
});
});
it('should handle arrays in schema', () => {
const schema = {
items: {
maxLength: 50,
type: 'string',
},
maxItems: 10,
minItems: 1,
type: 'array',
uniqueItems: true,
};
const result = filterAdvancedFields(schema);
expect(result).toEqual({
items: {
type: 'string',
},
type: 'array',
});
});
it('should handle arrays of objects', () => {
const schema = {
items: {
properties: {
age: {
maximum: 120,
minimum: 0,
type: 'number',
},
name: {
maxLength: 100,
type: 'string',
},
},
type: 'object',
},
type: 'array',
};
const result = filterAdvancedFields(schema);
expect(result).toEqual({
items: {
properties: {
age: {
type: 'number',
},
name: {
type: 'string',
},
},
type: 'object',
},
type: 'array',
});
});
it('should handle null values', () => {
const result = filterAdvancedFields(null);
expect(result).toBeNull();
});
it('should handle primitive values', () => {
expect(filterAdvancedFields('string')).toBe('string');
expect(filterAdvancedFields(123)).toBe(123);
expect(filterAdvancedFields(true)).toBe(true);
});
it('should handle empty objects', () => {
const schema = {};
const result = filterAdvancedFields(schema);
expect(result).toEqual({});
});
it('should preserve required and other common fields', () => {
const schema = {
additionalProperties: false,
description: 'A person object',
maxProperties: 10,
properties: {
age: {
description: 'Person age',
maximum: 150,
type: 'number',
},
name: {
description: 'Person name',
maxLength: 100,
type: 'string',
},
},
required: ['name', 'age'],
type: 'object',
};
const result = filterAdvancedFields(schema);
expect(result).toEqual({
additionalProperties: false,
description: 'A person object',
properties: {
age: {
description: 'Person age',
type: 'number',
},
name: {
description: 'Person name',
type: 'string',
},
},
required: ['name', 'age'],
type: 'object',
});
});
});
describe('Debug Configuration', () => {
it('should disable debug by default', () => {
delete process.env.DEBUG_GROQ_CHAT_COMPLETION;
@@ -13,6 +13,49 @@ export interface GroqModelCard {
id: string;
}
/**
* Filter out advanced JSON Schema properties that Groq doesn't support
*/
const filterAdvancedFields = (schema: any): any => {
if (typeof schema !== 'object' || schema === null) {
return schema;
}
if (Array.isArray(schema)) {
return schema.map(filterAdvancedFields);
}
const filtered: any = {};
// List of advanced properties to filter out
const unsupportedProperties = new Set([
'maxItems',
'minItems',
'maxLength',
'minLength',
'pattern',
'format',
'uniqueItems',
'maxProperties',
'minProperties',
'multipleOf',
'maximum',
'minimum',
'exclusiveMaximum',
'exclusiveMinimum',
]);
for (const [key, value] of Object.entries(schema)) {
if (unsupportedProperties.has(key)) {
continue;
}
filtered[key] = filterAdvancedFields(value);
}
return filtered;
};
export const params = {
baseURL: 'https://api.groq.com/openai/v1',
chatCompletion: {
@@ -40,6 +83,9 @@ export const params = {
debug: {
chatCompletion: () => process.env.DEBUG_GROQ_CHAT_COMPLETION === '1',
},
generateObject: {
handleSchema: filterAdvancedFields,
},
models: async ({ client }) => {
const { LOBE_DEFAULT_MODEL_LIST } = await import('model-bank');
@@ -0,0 +1,48 @@
import type { ChatMessage } from '@lobechat/types';
export const DEFAULT_AUTO_SUGGESTION =
'Generate relevant follow-up questions that naturally extend the conversation. Focus on exploring different aspects of the topic, providing actionable next steps, or diving deeper into related areas.';
export interface AutoSuggestionPromptOptions {
customPrompt?: string;
maxSuggestions?: number;
messages: ChatMessage[];
systemRole?: string;
}
/**
* Build auto-suggestion prompt based on conversation context
*/
export const autoSuggestionPrompt = (options: AutoSuggestionPromptOptions): string => {
const { messages, systemRole, customPrompt, maxSuggestions = 3 } = options;
// Get the last 5 messages for context
const recentMessages = messages.slice(-5);
// Build context from recent messages
const contextMessages = recentMessages
.map((msg) => {
const role = msg.role === 'assistant' ? 'Assistant' : 'User';
return `${role}: ${msg.content}`;
})
.join('\n');
return `Based on this conversation context and the assistant's role, generate ${maxSuggestions} relevant follow-up questions that a user might want to ask next.
${systemRole ? `Assistant Role: ${systemRole}` : ''}
Recent conversation:
${contextMessages}
Generate ${maxSuggestions} concise, relevant questions (each under 60 characters) that naturally follow from the conversation.
${customPrompt ? `Additional guidance: ${customPrompt}` : ''}
Requirements:
- Questions should be specific and actionable
- Avoid generic questions like "What else?" or "Tell me more"
- Focus on exploring different aspects of the topic
- Make questions diverse (not similar to each other)
Return the suggestions in the specified JSON format.`;
};
+1
View File
@@ -1,3 +1,4 @@
export * from './autoSuggestion';
export * from './chatMessages';
export * from './files';
export * from './groupChat';
+24
View File
@@ -7,6 +7,19 @@ export interface WorkingModel {
model: string;
provider: string;
}
export interface LobeAutoSuggestion {
/**
* Enable auto suggestions
* @default true
*/
enabled?: boolean;
/**
* Maximum number of suggestions to generate
* Range: 1-3 (based on issue #889)
* @default 3
*/
maxSuggestions?: number;
}
export interface LobeAgentChatConfig {
displayMode?: 'chat' | 'docs';
@@ -61,11 +74,22 @@ export interface LobeAgentChatConfig {
searchFCModel?: WorkingModel;
urlContext?: boolean;
useModelBuiltinSearch?: boolean;
/**
* Auto suggestion configuration
*/
autoSuggestion?: LobeAutoSuggestion;
}
/* eslint-enable */
export const AgentChatConfigSchema = z.object({
autoCreateTopicThreshold: z.number().default(2),
autoSuggestion: z
.object({
enabled: z.boolean().optional(),
maxSuggestions: z.number().min(1).max(3).optional(),
})
.optional(),
displayMode: z.enum(['chat', 'docs']).optional(),
enableAutoCreateTopic: z.boolean().optional(),
enableCompressHistory: z.boolean().optional(),
+8 -1
View File
@@ -93,7 +93,14 @@ export interface ModelSpeed {
latency?: number;
}
export interface MessageMetadata extends ModelUsage, ModelSpeed {}
export interface AutoSuggestionsUserActions {
choice: number;
suggestions: string[];
}
export interface MessageMetadata extends ModelUsage, ModelSpeed {
autoSuggestions?: AutoSuggestionsUserActions;
}
export type MessageRoleType = 'user' | 'system' | 'assistant' | 'tool' | 'supervisor';
+15
View File
@@ -37,7 +37,22 @@ export interface ChatFileChunk {
text: string;
}
export interface ChatAutoSuggestions {
/**
* Whether suggestions are currently being generated
*/
loading?: boolean;
/**
* List of suggested questions
*/
suggestions: string[];
}
export interface ChatMessageExtra {
/**
* Auto-generated suggestions for follow-up questions
*/
autoSuggestions?: ChatAutoSuggestions;
fromModel?: string;
fromProvider?: string;
// 翻译
@@ -11,6 +11,7 @@ export interface QueryRewriteSystemAgent extends Omit<SystemAgentItem, 'enabled'
export interface UserSystemAgentConfig {
agentMeta: SystemAgentItem;
autoSuggestion: SystemAgentItem;
generationTopic: SystemAgentItem;
groupChatSupervisor: SystemAgentItem;
historyCompress: SystemAgentItem;
@@ -1,6 +1,6 @@
'use client';
import { DEFAULT_REWRITE_QUERY } from '@lobechat/prompts';
import { DEFAULT_AUTO_SUGGESTION, DEFAULT_REWRITE_QUERY } from '@lobechat/prompts';
import { isServerMode } from '@/const/version';
import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
@@ -24,6 +24,12 @@ const Page = () => {
systemAgentKey="queryRewrite"
/>
)}
<SystemAgentForm
allowCustomPrompt
allowDisable
defaultPrompt={DEFAULT_AUTO_SUGGESTION}
systemAgentKey="autoSuggestion"
/>
</>
);
};
+30 -2
View File
@@ -1,7 +1,7 @@
'use client';
import { Form, type FormGroupItemType, ImageSelect, SliderWithInput, TextArea } from '@lobehub/ui';
import { Switch } from 'antd';
import { Form as AForm, Radio, Switch } from 'antd';
import { useThemeMode } from 'antd-style';
import isEqual from 'fast-deep-equal';
import { LayoutList, MessagesSquare } from 'lucide-react';
@@ -13,13 +13,15 @@ import { imageUrl } from '@/const/url';
import { selectors, useStore } from '../store';
const AUTO_SUGGESTION_SETTING_KEY = 'autoSuggestion';
const AgentChat = memo(() => {
const { t } = useTranslation('setting');
const [form] = Form.useForm();
const { isDarkMode } = useThemeMode();
const updateConfig = useStore((s) => s.setChatConfig);
const config = useStore(selectors.currentChatConfig, isEqual);
const enableAutoSuggestion = AForm.useWatch([AUTO_SUGGESTION_SETTING_KEY, 'enabled'], form);
const chat: FormGroupItemType = {
children: [
{
@@ -99,6 +101,32 @@ const AgentChat = memo(() => {
name: 'enableCompressHistory',
valuePropName: 'checked',
},
// 基础设置
{
children: <Switch />,
desc: t('agent.autoSuggestion.enabled.desc'),
label: t('agent.autoSuggestion.enabled.title'),
layout: 'horizontal',
minWidth: undefined,
name: [AUTO_SUGGESTION_SETTING_KEY, 'enabled'],
valuePropName: 'checked',
},
{
children: (
<Radio.Group
optionType="button"
options={[
{ label: '1', value: 1 },
{ label: '2', value: 2 },
{ label: '3', value: 3 },
]}
/>
),
desc: t('agent.autoSuggestion.maxSuggestions.desc'),
hidden: !enableAutoSuggestion,
label: t('agent.autoSuggestion.maxSuggestions.title'),
name: [AUTO_SUGGESTION_SETTING_KEY, 'maxSuggestions'],
},
],
title: t('settingChat.title'),
};
@@ -0,0 +1,85 @@
import { Block } from '@lobehub/ui';
import { createStyles } from 'antd-style';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { Flexbox } from 'react-layout-kit';
import BubblesLoading from '@/components/BubblesLoading';
import { useChatStore } from '@/store/chat';
import { ChatAutoSuggestions } from '@/types/message';
const useStyles = createStyles(({ css, token, responsive }) => ({
chip: css`
width: fit-content;
padding-block: 8px;
padding-inline: 16px;
border-radius: 48px;
background: ${token.colorBgContainerSecondary};
${responsive.mobile} {
padding-block: 8px;
padding-inline: 12px;
}
&:hover {
background: ${token.colorBgContainer};
}
`,
container: css`
margin-block-start: -32px;
`,
title: css`
color: ${token.colorTextDescription};
`,
}));
interface AutoSuggestionsProps extends ChatAutoSuggestions {
id: string;
}
export const AutoSuggestions = memo<AutoSuggestionsProps>(({ suggestions, loading, id }) => {
const { t } = useTranslation('chat');
const { styles } = useStyles();
const [sendMessage, updateAutoSuggestionChoices] = useChatStore((s) => [
s.sendMessage,
s.updateAutoSuggestionChoices,
]);
if (loading) {
return (
<Flexbox align={'center'} className={styles.container} horizontal>
<Flexbox className={styles.chip} flex={1} height={28} justify={'center'}>
<BubblesLoading />
</Flexbox>
</Flexbox>
);
}
if (!suggestions || suggestions.length === 0) {
return null;
}
return (
<div className={styles.container}>
<p className={styles.title}>{t('suggestions.title')}</p>
<Flexbox gap={12}>
{suggestions.map((q, index) => (
<Block
className={styles.chip}
clickable
key={q}
onClick={async () => {
sendMessage({ message: q });
updateAutoSuggestionChoices(id, { choice: index, suggestions });
}}
variant={'outlined'}
>
{q}
</Block>
))}
</Flexbox>
</div>
);
});
export default AutoSuggestions;
@@ -0,0 +1,18 @@
import { ChatMessageExtra } from '@lobechat/types';
import { memo } from 'react';
import { AutoSuggestions } from './AutoSuggestions';
interface BelowProps {
extra?: ChatMessageExtra;
id: string;
}
const Below = memo<BelowProps>(({ extra = {}, id }) => {
return (
extra && (
<div>{extra.autoSuggestions && <AutoSuggestions id={id} {...extra.autoSuggestions} />}</div>
)
);
});
export default Below;
@@ -32,6 +32,7 @@ import { markdownElements } from '../../MarkdownElements';
import { useDoubleClickEdit } from '../../hooks/useDoubleClickEdit';
import { normalizeThinkTags, processWithArtifact } from '../../utils';
import { AssistantActionsBar } from './Actions';
import Below from './Below';
import { AssistantMessageExtra } from './Extra';
import { AssistantMessageContent } from './MessageContent';
@@ -272,6 +273,7 @@ const AssistantMessage = memo<AssistantMessageProps>((props) => {
<AssistantActionsBar data={props} id={id} index={index} />
</Flexbox>
)}
<Below extra={extra} id={id} />
</Flexbox>
</Flexbox>
{mobile && <BorderSpacing borderSpacing={MOBILE_AVATAR_SIZE} />}
+8 -1
View File
@@ -15,6 +15,10 @@ export default {
thought: '思考过程',
unknownTitle: '未命名作品',
},
autoSuggestions: {
generating: '生成建议中...',
title: '建议问题',
},
availableAgents: '可用助手',
backToBottom: '跳转至当前',
chatList: {
@@ -250,7 +254,6 @@ export default {
senderAssistant: '助手',
senderUser: '你',
},
newAgent: '新建助手',
newGroupChat: '新建群聊',
@@ -282,6 +285,7 @@ export default {
},
regenerate: '重新生成',
roleAndArchive: '角色与记录',
search: {
grounding: {
@@ -353,6 +357,9 @@ export default {
loading: '识别中...',
prettifying: '润色中...',
},
suggestions: {
title: '试试继续问:',
},
supervisor: {
todoList: {
allComplete: '所有任务已完成',
+25
View File
@@ -2,7 +2,27 @@ export default {
about: {
title: '关于',
},
agent: {
autoSuggestion: {
customPrompt: {
desc: '指导 AI 生成什么类型的问题',
placeholder: '例如:侧重于技术实现细节...',
title: '自定义提示词',
},
enabled: {
desc: '在助手回复后自动生成后续问题建议',
title: '启用自动建议',
},
maxSuggestions: {
desc: '显示建议的数量(1-3',
title: '建议数量',
},
submit: '更新设置',
title: '自动建议',
},
},
agentTab: {
autoSuggestion: '自动建议',
chat: '聊天偏好',
meta: '助手信息',
modal: '模型设置',
@@ -620,6 +640,11 @@ export default {
modelDesc: '指定用于生成助理名称、描述、头像、标签的模型',
title: '自动生成助理信息',
},
autoSuggestion: {
label: '智能建议模型',
modelDesc: '指定用于生成消息智能建议的模型',
title: '消息智能建议',
},
customPrompt: {
addPrompt: '添加自定义提示',
desc: '填写后,系统助理将在生成内容时使用自定义提示',
+4 -2
View File
@@ -3,6 +3,8 @@ import { UserSystemAgentConfig } from '@/types/user/settings';
const protectedKeys = Object.keys(DEFAULT_SYSTEM_AGENT_CONFIG);
const defaultTrueLey = new Set(['queryRewrite', 'autoSuggestion']);
export const parseSystemAgent = (envString: string = ''): Partial<UserSystemAgentConfig> => {
if (!envString) return {};
@@ -38,7 +40,7 @@ export const parseSystemAgent = (envString: string = ''): Partial<UserSystemAgen
if (protectedKeys.includes(key)) {
config[key as keyof UserSystemAgentConfig] = {
enabled: key === 'queryRewrite' ? true : undefined,
enabled: defaultTrueLey.has(key) ? true : undefined,
model: model.trim(),
provider: provider.trim(),
} as any;
@@ -53,7 +55,7 @@ export const parseSystemAgent = (envString: string = ''): Partial<UserSystemAgen
for (const key of protectedKeys) {
if (!config[key as keyof UserSystemAgentConfig]) {
config[key as keyof UserSystemAgentConfig] = {
enabled: key === 'queryRewrite' ? true : undefined,
enabled: defaultTrueLey.has(key) ? true : undefined,
model: defaultSetting.model,
provider: defaultSetting.provider,
} as any;
+11
View File
@@ -198,6 +198,17 @@ export const messageRouter = router({
await ctx.messageModel.updateMessageRAG(input.id, input.value);
}),
updateMetadata: messageProcedure
.input(
z.object({
id: z.string(),
value: z.object({}).passthrough(),
}),
)
.mutation(async ({ input, ctx }) => {
return ctx.messageModel.updateMetadata(input.id, input.value);
}),
updatePluginError: messageProcedure
.input(
z.object({
+317
View File
@@ -0,0 +1,317 @@
import { ContextEngine } from '@lobechat/context-engine';
import { autoSuggestionPrompt } from '@lobechat/prompts';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { lambdaClient } from '@/libs/trpc/client';
import { createXorKeyVaultsPayload } from '@/services/_auth';
import { useUserStore } from '@/store/user';
import { systemAgentSelectors } from '@/store/user/slices/settings/selectors';
import { ChatMessage } from '@/types/message';
import { aiChatService } from './aiChat';
vi.mock('@lobechat/context-engine');
vi.mock('@lobechat/prompts');
vi.mock('@/libs/trpc/client', () => ({
lambdaClient: {
aiChat: {
outputJSON: {
mutate: vi.fn(),
},
},
},
}));
vi.mock('@/services/_auth');
vi.mock('@/store/user');
vi.mock('@/store/user/slices/settings/selectors');
describe('AiChatService', () => {
describe('generateSuggestion', () => {
const mockMessages: ChatMessage[] = [
{
id: 'msg1',
role: 'user',
content: 'Hello',
createdAt: Date.now(),
updatedAt: Date.now(),
meta: {},
},
{
id: 'msg2',
role: 'assistant',
content: 'Hi there! How can I help you?',
createdAt: Date.now(),
updatedAt: Date.now(),
meta: {},
},
] as ChatMessage[];
const mockSystemAgentConfig = {
enabled: true,
model: 'gpt-4',
provider: 'openai',
customPrompt: '',
};
const mockAbortController = new AbortController();
beforeEach(() => {
vi.clearAllMocks();
// Mock useUserStore.getState
vi.mocked(useUserStore.getState).mockReturnValue({} as any);
// Mock systemAgentSelectors.autoSuggestion
vi.mocked(systemAgentSelectors.autoSuggestion).mockReturnValue(mockSystemAgentConfig);
// Mock autoSuggestionPrompt
vi.mocked(autoSuggestionPrompt).mockReturnValue('Generated prompt text');
// Mock ContextEngine
const mockProcess = vi.fn().mockResolvedValue({
messages: [
{
content: 'Generated prompt text',
createdAt: Date.now(),
id: 'temp-suggestion-msg',
meta: {},
role: 'user',
updatedAt: Date.now(),
},
],
});
vi.mocked(ContextEngine).mockImplementation(
() =>
({
process: mockProcess,
}) as any,
);
// Mock createXorKeyVaultsPayload
vi.mocked(createXorKeyVaultsPayload).mockReturnValue('mock-payload' as any);
});
it('should generate suggestions successfully', async () => {
const mockSuggestions = {
suggestions: ['What can you do?', 'Tell me more', 'How does this work?'],
};
vi.mocked(lambdaClient.aiChat.outputJSON.mutate).mockResolvedValue(mockSuggestions);
const result = await aiChatService.generateSuggestion(
{
messages: mockMessages,
systemRole: 'You are a helpful assistant',
maxSuggestions: 3,
},
mockAbortController,
);
expect(result).toEqual(mockSuggestions.suggestions);
expect(autoSuggestionPrompt).toHaveBeenCalledWith({
customPrompt: '',
maxSuggestions: 3,
messages: mockMessages,
systemRole: 'You are a helpful assistant',
});
expect(lambdaClient.aiChat.outputJSON.mutate).toHaveBeenCalledWith(
expect.objectContaining({
messages: expect.any(Array),
model: 'gpt-4',
provider: 'openai',
schema: expect.objectContaining({
name: 'suggestions',
schema: expect.objectContaining({
properties: {
suggestions: {
items: { type: 'string' },
maxItems: 3,
type: 'array',
},
},
}),
}),
}),
{
context: { showNotification: false },
signal: mockAbortController.signal,
},
);
});
it('should return undefined when auto-suggestions are disabled', async () => {
vi.mocked(systemAgentSelectors.autoSuggestion).mockReturnValue({
...mockSystemAgentConfig,
enabled: false,
});
const result = await aiChatService.generateSuggestion(
{
messages: mockMessages,
systemRole: 'You are a helpful assistant',
},
mockAbortController,
);
expect(result).toBeUndefined();
expect(lambdaClient.aiChat.outputJSON.mutate).not.toHaveBeenCalled();
});
it('should process messages using ContextEngine', async () => {
const mockSuggestions = { suggestions: ['Suggestion 1'] };
vi.mocked(lambdaClient.aiChat.outputJSON.mutate).mockResolvedValue(mockSuggestions);
const mockContextEngine = {
process: vi.fn().mockResolvedValue({
messages: [
{
content: 'Processed prompt',
createdAt: Date.now(),
id: 'temp-suggestion-msg',
meta: {},
role: 'user',
updatedAt: Date.now(),
},
],
}),
};
vi.mocked(ContextEngine).mockImplementation(() => mockContextEngine as any);
await aiChatService.generateSuggestion(
{
messages: mockMessages,
},
mockAbortController,
);
expect(ContextEngine).toHaveBeenCalledWith(
expect.objectContaining({
pipeline: expect.any(Array),
}),
);
expect(mockContextEngine.process).toHaveBeenCalledWith(
expect.objectContaining({
initialState: {
messages: [],
},
maxTokens: 1024,
messages: expect.any(Array),
model: 'gpt-4',
}),
);
});
it('should handle custom prompt in system agent config', async () => {
const customPrompt = 'Custom suggestion prompt';
vi.mocked(systemAgentSelectors.autoSuggestion).mockReturnValue({
...mockSystemAgentConfig,
customPrompt,
});
const mockSuggestions = { suggestions: ['Custom suggestion'] };
vi.mocked(lambdaClient.aiChat.outputJSON.mutate).mockResolvedValue(mockSuggestions);
await aiChatService.generateSuggestion(
{
messages: mockMessages,
maxSuggestions: 2,
},
mockAbortController,
);
expect(autoSuggestionPrompt).toHaveBeenCalledWith({
customPrompt,
maxSuggestions: 2,
messages: mockMessages,
systemRole: undefined,
});
});
it('should create correct XOR key vaults payload', async () => {
const mockSuggestions = { suggestions: ['Test'] };
vi.mocked(lambdaClient.aiChat.outputJSON.mutate).mockResolvedValue(mockSuggestions);
await aiChatService.generateSuggestion(
{
messages: mockMessages,
},
mockAbortController,
);
expect(createXorKeyVaultsPayload).toHaveBeenCalledWith('openai');
expect(lambdaClient.aiChat.outputJSON.mutate).toHaveBeenCalledWith(
expect.objectContaining({
keyVaultsPayload: 'mock-payload',
}),
expect.any(Object),
);
});
it('should handle abort controller signal', async () => {
const abortController = new AbortController();
const mockSuggestions = { suggestions: ['Test'] };
vi.mocked(lambdaClient.aiChat.outputJSON.mutate).mockResolvedValue(mockSuggestions);
await aiChatService.generateSuggestion(
{
messages: mockMessages,
},
abortController,
);
expect(lambdaClient.aiChat.outputJSON.mutate).toHaveBeenCalledWith(
expect.any(Object),
expect.objectContaining({
signal: abortController.signal,
}),
);
});
it('should throw error when AI service fails', async () => {
const mockError = new Error('AI service error');
vi.mocked(lambdaClient.aiChat.outputJSON.mutate).mockRejectedValue(mockError);
await expect(
aiChatService.generateSuggestion(
{
messages: mockMessages,
},
mockAbortController,
),
).rejects.toThrow('AI service error');
});
it('should handle empty suggestions response', async () => {
const mockSuggestions = { suggestions: [] };
vi.mocked(lambdaClient.aiChat.outputJSON.mutate).mockResolvedValue(mockSuggestions);
const result = await aiChatService.generateSuggestion(
{
messages: mockMessages,
},
mockAbortController,
);
expect(result).toEqual([]);
});
it('should use default maxSuggestions when not provided', async () => {
const mockSuggestions = { suggestions: ['Test'] };
vi.mocked(lambdaClient.aiChat.outputJSON.mutate).mockResolvedValue(mockSuggestions);
await aiChatService.generateSuggestion(
{
messages: mockMessages,
},
mockAbortController,
);
expect(autoSuggestionPrompt).toHaveBeenCalledWith({
customPrompt: '',
maxSuggestions: undefined,
messages: mockMessages,
systemRole: undefined,
});
});
});
});
+83
View File
@@ -1,8 +1,37 @@
import { ContextEngine, MessageCleanupProcessor } from '@lobechat/context-engine';
import { autoSuggestionPrompt } from '@lobechat/prompts';
import { SendMessageServerParams, StructureOutputParams } from '@lobechat/types';
import { cleanObject } from '@lobechat/utils';
import { lambdaClient } from '@/libs/trpc/client';
import { createXorKeyVaultsPayload } from '@/services/_auth';
import { useUserStore } from '@/store/user';
import { systemAgentSelectors } from '@/store/user/slices/settings/selectors';
import { ChatMessage } from '@/types/message';
const SuggestionsSchema = {
description: 'Auto-generated suggestions for chat messages',
name: 'suggestions',
schema: {
additionalProperties: false,
properties: {
suggestions: {
items: { type: 'string' },
maxItems: 3,
type: 'array',
},
},
required: ['suggestions'],
type: 'object' as const,
},
strict: true,
};
interface GenerateSuggestionParams {
maxSuggestions?: number;
messages: ChatMessage[];
systemRole?: string;
}
class AiChatService {
sendMessageInServer = async (
@@ -28,6 +57,60 @@ class AiChatService {
);
};
generateSuggestion = async (
params: GenerateSuggestionParams,
abortController: AbortController,
): Promise<string[] | undefined> => {
const { maxSuggestions, messages, systemRole } = params;
// Get system agent configuration for model and provider
const userState = useUserStore.getState();
const systemAgentConfig = systemAgentSelectors.autoSuggestion(userState);
const { model, provider, customPrompt, enabled } = systemAgentConfig;
if (enabled === false) return;
// Build prompt using Prompt Layer
const prompt = autoSuggestionPrompt({ customPrompt, maxSuggestions, messages, systemRole });
// Process messages with ContextEngine
const contextEngine = new ContextEngine({
pipeline: [new MessageCleanupProcessor()],
});
const { messages: processedMessages } = await contextEngine.process({
initialState: {
messages: [],
},
maxTokens: 1024,
messages: [
{
content: prompt,
createdAt: Date.now(),
id: 'temp-suggestion-msg',
meta: {},
role: 'user',
updatedAt: Date.now(),
} as any,
],
model,
});
// Call AI service
const result = (await this.generateJSON(
{
messages: processedMessages,
model,
provider,
schema: SuggestionsSchema,
},
abortController,
)) as { suggestions: string[] };
// Parse suggestions
return result.suggestions;
};
// sendGroupMessageInServer = async (params: SendMessageServerParams) => {
// return lambdaClient.aiChat.sendGroupMessageInServer.mutate(cleanObject(params));
// };
+79 -2
View File
@@ -1,14 +1,29 @@
import { describe, expect, it } from 'vitest';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { INBOX_SESSION_ID } from '@/const/session';
import { lambdaClient } from '@/libs/trpc/client';
import { ServerService } from '../server';
vi.mock('@/libs/trpc/client', () => ({
lambdaClient: {
message: {
updateMetadata: {
mutate: vi.fn(),
},
},
},
}));
describe('ServerService', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('toDbSessionId', () => {
const service = new ServerService();
// @ts-ignore access private method for testing
const toDbSessionId = service.toDbSessionId;
const toDbSessionId = service['toDbSessionId'];
it('should return null for INBOX_SESSION_ID', () => {
expect(toDbSessionId(INBOX_SESSION_ID)).toBeNull();
@@ -41,4 +56,66 @@ describe('ServerService', () => {
expect(toDbSessionId(null as any)).toBeNull(); // Cast null to any to bypass type errors
});
});
describe('updateMessageMetadata', () => {
const service = new ServerService();
const mockMessageId = 'msg-123';
it('should call lambdaClient.message.updateMetadata.mutate with correct parameters', async () => {
const metadata = {
autoSuggestions: {
choice: 0,
suggestions: ['What can you do?', 'Tell me more', 'How does this work?'],
},
};
vi.mocked(lambdaClient.message.updateMetadata.mutate).mockResolvedValue(undefined);
await service.updateMessageMetadata(mockMessageId, metadata);
expect(lambdaClient.message.updateMetadata.mutate).toHaveBeenCalledWith({
id: mockMessageId,
value: metadata,
});
expect(lambdaClient.message.updateMetadata.mutate).toHaveBeenCalledTimes(1);
});
it('should handle empty metadata object', async () => {
vi.mocked(lambdaClient.message.updateMetadata.mutate).mockResolvedValue(undefined);
await service.updateMessageMetadata(mockMessageId, {});
expect(lambdaClient.message.updateMetadata.mutate).toHaveBeenCalledWith({
id: mockMessageId,
value: {},
});
});
it('should handle complex metadata objects', async () => {
const complexMetadata = {
autoSuggestions: {
choice: 1,
suggestions: ['Suggestion 1', 'Suggestion 2'],
},
} as any;
vi.mocked(lambdaClient.message.updateMetadata.mutate).mockResolvedValue(undefined);
await service.updateMessageMetadata(mockMessageId, complexMetadata);
expect(lambdaClient.message.updateMetadata.mutate).toHaveBeenCalledWith({
id: mockMessageId,
value: complexMetadata,
});
});
it('should throw error when tRPC mutation fails', async () => {
const mockError = new Error('Network error');
vi.mocked(lambdaClient.message.updateMetadata.mutate).mockRejectedValue(mockError);
await expect(service.updateMessageMetadata(mockMessageId, {})).rejects.toThrow(
'Network error',
);
});
});
});
+5
View File
@@ -68,6 +68,11 @@ export class ClientService implements IMessageService {
throw new Error('Method not implemented.');
}
// @ts-ignore
async updateMessageMetadata(): Promise<any> {
throw new Error('Method not implemented.');
}
// @ts-ignore
async countWords(): Promise<number> {
throw new Error('Method not implemented.');
+135 -33
View File
@@ -1,5 +1,5 @@
import { and, eq } from 'drizzle-orm';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { clientDB, initializeDB } from '@/database/client/db';
import {
@@ -13,7 +13,6 @@ import {
users,
} from '@/database/schemas';
import {
ChatMessage,
ChatMessageError,
ChatTTS,
ChatTranslate,
@@ -29,14 +28,6 @@ const topicId = 'topic-id';
// Mock data
const mockMessageId = 'mock-message-id';
const mockMessage = {
id: mockMessageId,
content: 'Mock message content',
sessionId,
role: 'user',
} as ChatMessage;
const mockMessages = [mockMessage];
beforeEach(async () => {
await initializeDB();
@@ -49,12 +40,12 @@ beforeEach(async () => {
await trx.insert(sessions).values([{ id: sessionId, userId }]);
await trx.insert(topics).values([{ id: topicId, sessionId, userId }]);
await trx.insert(files).values({
id: 'f1',
userId: userId,
url: 'abc',
name: 'file-1',
fileType: 'image/png',
id: 'f1',
name: 'file-1',
size: 1000,
url: 'abc',
userId: userId,
});
});
});
@@ -72,8 +63,8 @@ describe('MessageClientService', () => {
// Setup
const createParams: CreateMessageParams = {
content: 'New message content',
sessionId,
role: 'user',
sessionId,
};
// Execute
@@ -90,13 +81,13 @@ describe('MessageClientService', () => {
await messageService.batchCreateMessages([
{
content: 'Mock message content',
sessionId,
role: 'user',
sessionId,
},
{
content: 'Mock message content',
sessionId,
role: 'user',
sessionId,
},
] as MessageItem[]);
const count = await clientDB.$count(messages);
@@ -141,7 +132,7 @@ describe('MessageClientService', () => {
// Setup
await clientDB
.insert(messages)
.values({ id: mockMessageId, sessionId, topicId, role: 'user', userId });
.values({ id: mockMessageId, role: 'user', sessionId, topicId, userId });
// Execute
const data = await messageService.getMessages(sessionId, topicId);
@@ -160,9 +151,9 @@ describe('MessageClientService', () => {
{ id: sessionId, userId },
]);
await clientDB.insert(messages).values([
{ sessionId, topicId, role: 'user', userId },
{ sessionId, topicId, role: 'assistant', userId },
{ sessionId: 'bbb', topicId, role: 'assistant', userId },
{ role: 'user', sessionId, topicId, userId },
{ role: 'assistant', sessionId, topicId, userId },
{ role: 'assistant', sessionId: 'bbb', topicId, userId },
]);
// Execute
@@ -176,16 +167,15 @@ describe('MessageClientService', () => {
describe('removeMessagesByAssistant', () => {
it('should batch remove messages by assistantId and topicId', async () => {
// Setup
const assistantId = 'assistant-id';
const sessionId = 'session-id';
await clientDB.insert(sessions).values([
{ id: 'bbb', userId },
{ id: sessionId, userId },
]);
await clientDB.insert(messages).values([
{ sessionId, topicId, role: 'user', userId },
{ sessionId, topicId, role: 'assistant', userId },
{ sessionId: 'bbb', topicId, role: 'assistant', userId },
{ role: 'user', sessionId, topicId, userId },
{ role: 'assistant', sessionId, topicId, userId },
{ role: 'assistant', sessionId: 'bbb', topicId, userId },
]);
// Execute
@@ -223,8 +213,8 @@ describe('MessageClientService', () => {
describe('getAllMessages', () => {
it('should retrieve all messages', async () => {
await clientDB.insert(messages).values([
{ sessionId, topicId, content: '1', role: 'user', userId },
{ sessionId, topicId, content: '2', role: 'assistant', userId },
{ content: '1', role: 'user', sessionId, topicId, userId },
{ content: '2', role: 'assistant', sessionId, topicId, userId },
]);
// Execute
@@ -232,8 +222,8 @@ describe('MessageClientService', () => {
// Assert
expect(data).toMatchObject([
{ sessionId, topicId, content: '1', role: 'user', userId },
{ sessionId, topicId, content: '2', role: 'assistant', userId },
{ content: '1', role: 'user', sessionId, topicId, userId },
{ content: '2', role: 'assistant', sessionId, topicId, userId },
]);
});
});
@@ -243,8 +233,8 @@ describe('MessageClientService', () => {
// Setup
await clientDB.insert(messages).values({ id: mockMessageId, role: 'user', userId });
const newError = {
type: 'InvalidProviderAPIKey',
message: 'Error occurred',
type: 'InvalidProviderAPIKey',
} as ChatMessageError;
// Execute
@@ -312,7 +302,7 @@ describe('MessageClientService', () => {
// Assert
const result = await clientDB.query.messagePlugins.findFirst({
where: eq(messageTTS.id, mockMessageId),
where: eq(messagePlugins.id, mockMessageId),
});
expect(result).toMatchObject({ arguments: '{"key":"stateValue"}' });
});
@@ -329,7 +319,7 @@ describe('MessageClientService', () => {
// Assert
const result = await clientDB.query.messagePlugins.findFirst({
where: eq(messageTTS.id, mockMessageId),
where: eq(messagePlugins.id, mockMessageId),
});
expect(result).toMatchObject({ arguments: '{"abc":"stateValue"}' });
});
@@ -353,7 +343,7 @@ describe('MessageClientService', () => {
// Setup
await clientDB
.insert(files)
.values({ id: 'file-abc', fileType: 'text', name: 'abc', url: 'abc', size: 100, userId });
.values({ fileType: 'text', id: 'file-abc', name: 'abc', size: 100, url: 'abc', userId });
await clientDB.insert(messages).values({ id: mockMessageId, role: 'user', userId });
const newTTS: ChatTTS = { contentMd5: 'abc', file: 'file-abc' };
@@ -407,4 +397,116 @@ describe('MessageClientService', () => {
expect(result).toBe(false);
});
});
describe('updateMessageMetadata', () => {
it('should update the metadata field of a message', async () => {
// Setup
await clientDB.insert(messages).values({ id: mockMessageId, role: 'user', userId });
const newMetadata = {
autoSuggestions: {
choice: 0,
suggestions: ['What can you do?', 'Tell me more'],
},
};
// Execute
await messageService.updateMessageMetadata(mockMessageId, newMetadata);
// Assert
const result = await clientDB.query.messages.findFirst({
where: eq(messages.id, mockMessageId),
});
expect(result!.metadata).toMatchObject(newMetadata);
});
it('should merge new metadata with existing metadata (shallow merge)', async () => {
// Setup
const existingMetadata = { customField: 'test' } as any;
await clientDB.insert(messages).values({
id: mockMessageId,
metadata: existingMetadata,
role: 'user',
userId,
});
const newMetadata = {
autoSuggestions: {
choice: 1,
suggestions: ['New suggestion'],
},
};
// Execute
await messageService.updateMessageMetadata(mockMessageId, newMetadata);
// Assert
const result = await clientDB.query.messages.findFirst({
where: eq(messages.id, mockMessageId),
});
expect(result!.metadata).toMatchObject({
...existingMetadata,
...newMetadata,
});
});
it('should deep merge nested metadata objects', async () => {
// Setup - existing metadata with nested structure
const existingMetadata = {
autoSuggestions: {
choice: 0,
suggestions: ['old1', 'old2'],
},
otherData: 'preserved',
};
await clientDB.insert(messages).values({
id: mockMessageId,
metadata: existingMetadata as any,
role: 'user',
userId,
});
// New metadata updates autoSuggestions and adds new field
const newMetadata = {
autoSuggestions: {
choice: 1,
suggestions: ['new1', 'new2', 'new3'],
},
newField: 'added',
};
// Execute
await messageService.updateMessageMetadata(mockMessageId, newMetadata);
// Assert - deep merge should update autoSuggestions and preserve otherData
const result = await clientDB.query.messages.findFirst({
where: eq(messages.id, mockMessageId),
});
expect(result!.metadata).toEqual({
autoSuggestions: {
choice: 1, // updated
suggestions: ['new1', 'new2', 'new3'], // updated
},
newField: 'added', // new field added
otherData: 'preserved', // existing field preserved
});
});
it('should handle empty metadata update', async () => {
// Setup
await clientDB.insert(messages).values({ id: mockMessageId, role: 'user', userId });
// Execute
await messageService.updateMessageMetadata(mockMessageId, {});
// Assert
const result = await clientDB.query.messages.findFirst({
where: eq(messages.id, mockMessageId),
});
expect(result).toBeTruthy();
});
});
});
+4
View File
@@ -100,6 +100,10 @@ export class ClientService extends BaseClientService implements IMessageService
return this.messageModel.update(id, message);
};
updateMessageMetadata: IMessageService['updateMessageMetadata'] = async (id, metadata) => {
return this.messageModel.updateMetadata(id, metadata);
};
updateMessageTTS: IMessageService['updateMessageTTS'] = async (id, tts) => {
return this.messageModel.updateTTS(id, tts as any);
};
+4
View File
@@ -85,6 +85,10 @@ export class ServerService implements IMessageService {
return lambdaClient.message.updateTTS.mutate({ id, value: tts });
};
updateMessageMetadata: IMessageService['updateMessageMetadata'] = async (id, value) => {
return lambdaClient.message.updateMetadata.mutate({ id, value });
};
updateMessagePluginState: IMessageService['updateMessagePluginState'] = async (id, value) => {
return lambdaClient.message.updatePluginState.mutate({ id, value });
};
+2
View File
@@ -8,6 +8,7 @@ import {
ChatTranslate,
CreateMessageParams,
MessageItem,
MessageMetadata,
ModelRankItem,
UpdateMessageParams,
} from '@/types/message';
@@ -37,6 +38,7 @@ export interface IMessageService {
getHeatmaps(): Promise<HeatmapsProps['data']>;
updateMessageError(id: string, error: ChatMessageError): Promise<any>;
updateMessage(id: string, message: Partial<UpdateMessageParams>): Promise<any>;
updateMessageMetadata(id: string, message: Partial<MessageMetadata>): Promise<any>;
updateMessageTTS(id: string, tts: Partial<ChatTTS> | false): Promise<any>;
updateMessageTranslate(id: string, translate: Partial<ChatTranslate> | false): Promise<any>;
updateMessagePluginState(id: string, value: Record<string, any>): Promise<any>;
@@ -0,0 +1,640 @@
import { act, renderHook } from '@testing-library/react';
import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { aiChatService } from '@/services/aiChat';
import { messageService } from '@/services/message';
import { useAgentStore } from '@/store/agent';
import { agentSelectors } from '@/store/agent/selectors';
import { chatSelectors } from '@/store/chat/selectors';
import { useChatStore } from '../../../../store';
import { TEST_CONTENT, TEST_IDS, createMockMessage } from './fixtures';
import { resetTestEnvironment, setupMockSelectors } from './helpers';
// Mock services
vi.mock('@/services/aiChat', () => ({
aiChatService: {
generateSuggestion: vi.fn(),
},
}));
vi.mock('@/services/message', () => ({
messageService: {
updateMessageMetadata: vi.fn(),
},
}));
vi.mock('zustand/traditional');
beforeEach(() => {
resetTestEnvironment();
setupMockSelectors();
vi.clearAllMocks();
// Mock internal_dispatchMessage as it's used by the actions
act(() => {
useChatStore.setState({
internal_dispatchMessage: vi.fn(),
});
});
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('autoSuggestion actions', () => {
describe('generateSuggestions', () => {
describe('validation', () => {
it('should not generate suggestions if message not found', async () => {
const { result } = renderHook(() => useChatStore());
act(() => {
useChatStore.setState({
messagesMap: { default: [] },
});
});
await act(async () => {
await result.current.generateSuggestions('non-existent-id');
});
expect(aiChatService.generateSuggestion).not.toHaveBeenCalled();
expect(result.current.internal_dispatchMessage).not.toHaveBeenCalled();
});
it('should not generate suggestions if message is not assistant role', async () => {
const { result } = renderHook(() => useChatStore());
const userMessage = createMockMessage({ id: TEST_IDS.MESSAGE_ID, role: 'user' });
vi.spyOn(chatSelectors, 'activeBaseChats').mockReturnValue([userMessage]);
await act(async () => {
await result.current.generateSuggestions(TEST_IDS.MESSAGE_ID);
});
expect(aiChatService.generateSuggestion).not.toHaveBeenCalled();
});
it('should not generate suggestions if auto-suggestion is disabled', async () => {
const { result } = renderHook(() => useChatStore());
const assistantMessage = createMockMessage({
id: TEST_IDS.MESSAGE_ID,
role: 'assistant',
});
vi.spyOn(chatSelectors, 'activeBaseChats').mockReturnValue([assistantMessage]);
setupMockSelectors({
agentConfig: {
chatConfig: {
autoSuggestion: {
enabled: false,
},
},
},
});
await act(async () => {
await result.current.generateSuggestions(TEST_IDS.MESSAGE_ID);
});
expect(aiChatService.generateSuggestion).not.toHaveBeenCalled();
});
});
describe('successful generation', () => {
it('should generate suggestions for assistant message', async () => {
const { result } = renderHook(() => useChatStore());
const assistantMessage = createMockMessage({
id: TEST_IDS.MESSAGE_ID,
role: 'assistant',
content: TEST_CONTENT.AI_RESPONSE,
});
const suggestions = ['Follow up question 1', 'Follow up question 2'];
vi.spyOn(chatSelectors, 'activeBaseChats').mockReturnValue([assistantMessage]);
setupMockSelectors({
agentConfig: {
systemRole: 'You are a helpful assistant',
chatConfig: {
autoSuggestion: {
enabled: true,
maxSuggestions: 3,
},
},
},
});
(aiChatService.generateSuggestion as Mock).mockResolvedValue(suggestions);
await act(async () => {
await result.current.generateSuggestions(TEST_IDS.MESSAGE_ID);
});
// Should set loading state initially
expect(result.current.internal_dispatchMessage).toHaveBeenCalledWith({
id: TEST_IDS.MESSAGE_ID,
key: 'autoSuggestions',
type: 'updateMessageExtra',
value: { loading: true, suggestions: [] },
});
// Should call service with correct parameters
expect(aiChatService.generateSuggestion).toHaveBeenCalledWith(
{
maxSuggestions: 3,
messages: [assistantMessage],
systemRole: 'You are a helpful assistant',
},
expect.any(AbortController),
);
// Should update with suggestions and clear loading
expect(result.current.internal_dispatchMessage).toHaveBeenCalledWith({
id: TEST_IDS.MESSAGE_ID,
key: 'autoSuggestions',
type: 'updateMessageExtra',
value: { loading: false, suggestions },
});
});
it('should pass agent configuration to service', async () => {
const { result } = renderHook(() => useChatStore());
const assistantMessage = createMockMessage({
id: TEST_IDS.MESSAGE_ID,
role: 'assistant',
});
vi.spyOn(chatSelectors, 'activeBaseChats').mockReturnValue([assistantMessage]);
setupMockSelectors({
agentConfig: {
systemRole: 'Custom system role',
chatConfig: {
autoSuggestion: {
enabled: true,
maxSuggestions: 5,
},
},
},
});
(aiChatService.generateSuggestion as Mock).mockResolvedValue(['suggestion']);
await act(async () => {
await result.current.generateSuggestions(TEST_IDS.MESSAGE_ID);
});
expect(aiChatService.generateSuggestion).toHaveBeenCalledWith(
expect.objectContaining({
maxSuggestions: 5,
systemRole: 'Custom system role',
}),
expect.any(AbortController),
);
});
});
describe('timeout handling', () => {
it('should abort request after 10 seconds', async () => {
const { result } = renderHook(() => useChatStore());
const assistantMessage = createMockMessage({
id: TEST_IDS.MESSAGE_ID,
role: 'assistant',
});
vi.spyOn(chatSelectors, 'activeBaseChats').mockReturnValue([assistantMessage]);
setupMockSelectors({
agentConfig: {
chatConfig: {
autoSuggestion: {
enabled: true,
maxSuggestions: 3,
},
},
},
});
let capturedController: AbortController | undefined;
(aiChatService.generateSuggestion as Mock).mockImplementation(
async (params, controller) => {
capturedController = controller;
// Simulate long-running request that gets aborted
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => resolve(['suggestion']), 15_000);
controller.signal.addEventListener('abort', () => {
clearTimeout(timeout);
reject(new Error('Request aborted'));
});
});
},
);
vi.useFakeTimers();
const promise = act(async () => {
try {
await result.current.generateSuggestions(TEST_IDS.MESSAGE_ID);
} catch {
// Expected to throw on abort
}
});
// Fast-forward to trigger timeout
await vi.advanceTimersByTimeAsync(10_000);
await promise;
expect(capturedController?.signal.aborted).toBe(true);
vi.useRealTimers();
});
});
describe('error handling', () => {
it('should silently fail and remove autoSuggestions on error', async () => {
const assistantMessage = createMockMessage({
id: TEST_IDS.MESSAGE_ID,
role: 'assistant',
});
vi.spyOn(chatSelectors, 'activeBaseChats').mockReturnValue([assistantMessage]);
setupMockSelectors({
agentConfig: {
chatConfig: {
autoSuggestion: {
enabled: true,
maxSuggestions: 3,
},
},
},
});
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
(aiChatService.generateSuggestion as Mock).mockRejectedValue(new Error('Network error'));
const { result } = renderHook(() => useChatStore());
const dispatchSpy = result.current.internal_dispatchMessage as any;
await act(async () => {
await result.current.generateSuggestions(TEST_IDS.MESSAGE_ID);
});
// Should set loading initially
expect(dispatchSpy).toHaveBeenCalledWith({
id: TEST_IDS.MESSAGE_ID,
key: 'autoSuggestions',
type: 'updateMessageExtra',
value: { loading: true, suggestions: [] },
});
// Should clear autoSuggestions on error
expect(dispatchSpy).toHaveBeenCalledWith({
id: TEST_IDS.MESSAGE_ID,
key: 'autoSuggestions',
type: 'updateMessageExtra',
value: undefined,
});
expect(consoleErrorSpy).toHaveBeenCalledWith(
'Failed to generate suggestions:',
expect.any(Error),
);
consoleErrorSpy.mockRestore();
});
it('should handle API timeout gracefully', async () => {
const assistantMessage = createMockMessage({
id: TEST_IDS.MESSAGE_ID,
role: 'assistant',
});
vi.spyOn(chatSelectors, 'activeBaseChats').mockReturnValue([assistantMessage]);
setupMockSelectors({
agentConfig: {
chatConfig: {
autoSuggestion: {
enabled: true,
maxSuggestions: 3,
},
},
},
});
const abortError = new Error('Request aborted');
abortError.name = 'AbortError';
(aiChatService.generateSuggestion as Mock).mockRejectedValue(abortError);
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const { result } = renderHook(() => useChatStore());
const dispatchSpy = result.current.internal_dispatchMessage as any;
await act(async () => {
await result.current.generateSuggestions(TEST_IDS.MESSAGE_ID);
});
// Should clear autoSuggestions on abort
expect(dispatchSpy).toHaveBeenCalledWith({
id: TEST_IDS.MESSAGE_ID,
key: 'autoSuggestions',
type: 'updateMessageExtra',
value: undefined,
});
consoleErrorSpy.mockRestore();
});
it('should handle empty suggestions response', async () => {
const assistantMessage = createMockMessage({
id: TEST_IDS.MESSAGE_ID,
role: 'assistant',
});
vi.spyOn(chatSelectors, 'activeBaseChats').mockReturnValue([assistantMessage]);
setupMockSelectors({
agentConfig: {
chatConfig: {
autoSuggestion: {
enabled: true,
maxSuggestions: 3,
},
},
},
});
(aiChatService.generateSuggestion as Mock).mockResolvedValue([]);
const { result } = renderHook(() => useChatStore());
const dispatchSpy = result.current.internal_dispatchMessage as any;
await act(async () => {
await result.current.generateSuggestions(TEST_IDS.MESSAGE_ID);
});
// Should update with empty suggestions array
expect(dispatchSpy).toHaveBeenCalledWith({
id: TEST_IDS.MESSAGE_ID,
key: 'autoSuggestions',
type: 'updateMessageExtra',
value: { loading: false, suggestions: [] },
});
});
});
});
describe('updateAutoSuggestionChoices', () => {
it('should update message metadata with user feedback', async () => {
const assistantMessage = createMockMessage({
id: TEST_IDS.MESSAGE_ID,
role: 'assistant',
});
vi.spyOn(chatSelectors, 'getMessageById').mockReturnValue(() => assistantMessage);
const userFeedback = {
choice: 1,
suggestions: ['Suggestion 1', 'Suggestion 2', 'Suggestion 3'],
};
const { result } = renderHook(() => useChatStore());
const dispatchSpy = result.current.internal_dispatchMessage as any;
await act(async () => {
await result.current.updateAutoSuggestionChoices(TEST_IDS.MESSAGE_ID, userFeedback);
});
// Should clear autoSuggestions from message extra
expect(dispatchSpy).toHaveBeenCalledWith({
id: TEST_IDS.MESSAGE_ID,
key: 'autoSuggestions',
type: 'updateMessageExtra',
value: undefined,
});
// Should persist user choice to message metadata
expect(messageService.updateMessageMetadata).toHaveBeenCalledWith(TEST_IDS.MESSAGE_ID, {
autoSuggestions: userFeedback,
});
});
it('should not update if message not found', async () => {
vi.spyOn(chatSelectors, 'getMessageById').mockReturnValue(() => undefined);
const userFeedback = {
choice: 0,
suggestions: ['Suggestion 1'],
};
const { result } = renderHook(() => useChatStore());
const dispatchSpy = result.current.internal_dispatchMessage as any;
await act(async () => {
await result.current.updateAutoSuggestionChoices('non-existent-id', userFeedback);
});
expect(dispatchSpy).not.toHaveBeenCalled();
expect(messageService.updateMessageMetadata).not.toHaveBeenCalled();
});
it('should handle different choice indexes', async () => {
const assistantMessage = createMockMessage({
id: TEST_IDS.MESSAGE_ID,
role: 'assistant',
});
vi.spyOn(chatSelectors, 'getMessageById').mockReturnValue(() => assistantMessage);
const suggestions = ['First option', 'Second option', 'Third option'];
const { result } = renderHook(() => useChatStore());
// Test choice 0
await act(async () => {
await result.current.updateAutoSuggestionChoices(TEST_IDS.MESSAGE_ID, {
choice: 0,
suggestions,
});
});
expect(messageService.updateMessageMetadata).toHaveBeenCalledWith(TEST_IDS.MESSAGE_ID, {
autoSuggestions: { choice: 0, suggestions },
});
// Test choice 2
await act(async () => {
await result.current.updateAutoSuggestionChoices(TEST_IDS.MESSAGE_ID, {
choice: 2,
suggestions,
});
});
expect(messageService.updateMessageMetadata).toHaveBeenCalledWith(TEST_IDS.MESSAGE_ID, {
autoSuggestions: { choice: 2, suggestions },
});
});
it('should update metadata even with single suggestion', async () => {
const assistantMessage = createMockMessage({
id: TEST_IDS.MESSAGE_ID,
role: 'assistant',
});
vi.spyOn(chatSelectors, 'getMessageById').mockReturnValue(() => assistantMessage);
const userFeedback = {
choice: 0,
suggestions: ['Only one suggestion'],
};
const { result } = renderHook(() => useChatStore());
await act(async () => {
await result.current.updateAutoSuggestionChoices(TEST_IDS.MESSAGE_ID, userFeedback);
});
expect(messageService.updateMessageMetadata).toHaveBeenCalledWith(TEST_IDS.MESSAGE_ID, {
autoSuggestions: userFeedback,
});
});
it('should handle metadata update errors gracefully', async () => {
const assistantMessage = createMockMessage({
id: TEST_IDS.MESSAGE_ID,
role: 'assistant',
});
vi.spyOn(chatSelectors, 'getMessageById').mockReturnValue(() => assistantMessage);
(messageService.updateMessageMetadata as Mock).mockRejectedValue(new Error('Update failed'));
const userFeedback = {
choice: 1,
suggestions: ['Suggestion 1', 'Suggestion 2'],
};
const { result } = renderHook(() => useChatStore());
const dispatchSpy = result.current.internal_dispatchMessage as any;
// Should throw error if update fails
await expect(
act(async () => {
await result.current.updateAutoSuggestionChoices(TEST_IDS.MESSAGE_ID, userFeedback);
}),
).rejects.toThrow('Update failed');
// Should still have attempted to clear UI state before error
expect(dispatchSpy).toHaveBeenCalledWith({
id: TEST_IDS.MESSAGE_ID,
key: 'autoSuggestions',
type: 'updateMessageExtra',
value: undefined,
});
});
});
describe('integration scenarios', () => {
it('should handle complete suggestion workflow', async () => {
const assistantMessage = createMockMessage({
id: TEST_IDS.MESSAGE_ID,
role: 'assistant',
content: 'AI response about weather',
});
vi.spyOn(chatSelectors, 'activeBaseChats').mockReturnValue([assistantMessage]);
vi.spyOn(chatSelectors, 'getMessageById').mockReturnValue(() => assistantMessage);
setupMockSelectors({
agentConfig: {
systemRole: 'Weather assistant',
chatConfig: {
autoSuggestion: {
enabled: true,
maxSuggestions: 3,
},
},
},
});
const suggestions = ['What about tomorrow?', 'Show me weekly forecast', 'Any rain today?'];
(aiChatService.generateSuggestion as Mock).mockResolvedValue(suggestions);
const { result } = renderHook(() => useChatStore());
const dispatchSpy = result.current.internal_dispatchMessage as any;
// Step 1: Generate suggestions
await act(async () => {
await result.current.generateSuggestions(TEST_IDS.MESSAGE_ID);
});
expect(dispatchSpy).toHaveBeenCalledWith({
id: TEST_IDS.MESSAGE_ID,
key: 'autoSuggestions',
type: 'updateMessageExtra',
value: { loading: false, suggestions },
});
// Step 2: User selects a suggestion
await act(async () => {
await result.current.updateAutoSuggestionChoices(TEST_IDS.MESSAGE_ID, {
choice: 1,
suggestions,
});
});
expect(dispatchSpy).toHaveBeenCalledWith({
id: TEST_IDS.MESSAGE_ID,
key: 'autoSuggestions',
type: 'updateMessageExtra',
value: undefined,
});
expect(messageService.updateMessageMetadata).toHaveBeenCalledWith(TEST_IDS.MESSAGE_ID, {
autoSuggestions: { choice: 1, suggestions },
});
});
it('should handle concurrent suggestion generations', async () => {
const message1 = createMockMessage({ id: 'msg-1', role: 'assistant' });
const message2 = createMockMessage({ id: 'msg-2', role: 'assistant' });
vi.spyOn(chatSelectors, 'activeBaseChats').mockReturnValue([message1, message2]);
setupMockSelectors({
agentConfig: {
chatConfig: {
autoSuggestion: {
enabled: true,
maxSuggestions: 2,
},
},
},
});
(aiChatService.generateSuggestion as Mock).mockResolvedValue(['suggestion']);
const { result } = renderHook(() => useChatStore());
// Generate suggestions for both messages concurrently
await act(async () => {
await Promise.all([
result.current.generateSuggestions('msg-1'),
result.current.generateSuggestions('msg-2'),
]);
});
// Both should have been processed
expect(aiChatService.generateSuggestion).toHaveBeenCalledTimes(2);
});
});
});
@@ -0,0 +1,104 @@
import { AutoSuggestionsUserActions, ChatMessage } from '@lobechat/types';
import { StateCreator } from 'zustand/vanilla';
import { aiChatService } from '@/services/aiChat';
import { messageService } from '@/services/message';
import { useAgentStore } from '@/store/agent';
import { agentSelectors } from '@/store/agent/selectors';
import { ChatStore } from '@/store/chat/store';
import { chatSelectors } from '../../../selectors';
export interface ChatAutoSuggestionAction {
clearAutoSuggestions: (id: string) => void;
/**
* Generate auto-suggestions for a message
*/
generateSuggestions: (messageId: string) => Promise<void>;
/**
* Update suggestions for a message
*/
updateAutoSuggestionChoices: (
id: string,
autoSuggestions: AutoSuggestionsUserActions,
) => Promise<void>;
}
export const chatAutoSuggestion: StateCreator<
ChatStore,
[['zustand/devtools', never]],
[],
ChatAutoSuggestionAction
> = (set, get) => ({
clearAutoSuggestions: (id) => {
get().internal_dispatchMessage({
id: id,
key: 'autoSuggestions',
type: 'updateMessageExtra',
value: undefined,
});
},
generateSuggestions: async (messageId: string) => {
const { internal_dispatchMessage } = get();
const messages = chatSelectors.activeBaseChats(get());
const message = messages.find((msg: ChatMessage) => msg.id === messageId);
if (!message || message.role !== 'assistant') return;
// Get agent configuration for systemRole and autoSuggestion config
const agentState = useAgentStore.getState();
const agentConfig = agentSelectors.currentAgentConfig(agentState);
// Check if auto-suggestions are enabled
if (!agentConfig.chatConfig.autoSuggestion?.enabled) return;
try {
// Set loading state
internal_dispatchMessage({
id: messageId,
key: 'autoSuggestions',
type: 'updateMessageExtra',
value: { loading: true, suggestions: [] },
});
// Call AI service with 10 second timeout
const abortController = new AbortController();
const timeoutId = setTimeout(() => abortController.abort(), 10_000);
const suggestions = await aiChatService.generateSuggestion(
{
maxSuggestions: agentConfig.chatConfig.autoSuggestion.maxSuggestions,
messages,
systemRole: agentConfig.systemRole,
},
abortController,
);
clearTimeout(timeoutId);
// Update message with suggestions
internal_dispatchMessage({
id: messageId,
key: 'autoSuggestions',
type: 'updateMessageExtra',
value: { loading: false, suggestions },
});
} catch (error) {
console.error('Failed to generate suggestions:', error);
// Silent failure: remove autoSuggestions completely
get().clearAutoSuggestions(messageId);
}
},
updateAutoSuggestionChoices: async (messageId: string, userFeedback) => {
const { clearAutoSuggestions } = get();
const message = chatSelectors.getMessageById(messageId)(get());
if (!message) return;
clearAutoSuggestions(messageId);
await messageService.updateMessageMetadata(messageId, { autoSuggestions: userFeedback });
},
});
@@ -394,7 +394,8 @@ export const generateAIChat: StateCreator<
)(aiInfraStoreState);
const useModelBuiltinSearch = agentChatConfigSelectors.useModelBuiltinSearch(agentStoreState);
const useModelSearch =
((isProviderHasBuiltinSearch || isModelHasBuiltinSearch) && useModelBuiltinSearch) || isModelBuiltinSearchInternal;
((isProviderHasBuiltinSearch || isModelHasBuiltinSearch) && useModelBuiltinSearch) ||
isModelBuiltinSearchInternal;
const isAgentEnableSearch = agentChatConfigSelectors.isAgentEnableSearch(agentStoreState);
if (isAgentEnableSearch && !useModelSearch && !isModelSupportToolUse) {
@@ -515,7 +516,14 @@ export const generateAIChat: StateCreator<
}
}
// 6. summary history if context messages is larger than historyCount
// 6. Generate auto-suggestions after message completion
get()
.generateSuggestions(assistantId)
.catch((error) => {
console.error('Error generating auto-suggestions:', error);
});
// 7. summary history if context messages is larger than historyCount
const historyCount = agentChatConfigSelectors.historyCount(agentStoreState);
if (
@@ -516,6 +516,14 @@ export const generateAIChatV2: StateCreator<
console.error('Desktop notification error:', error);
}
}
// Generate auto-suggestions after message completion
if (!isFunctionCall) {
get()
.generateSuggestions(assistantId)
.catch((error) => {
console.error('Error generating auto-suggestions:', error);
});
}
}
// 6. summary history if context messages is larger than historyCount
@@ -2,6 +2,7 @@ import { StateCreator } from 'zustand/vanilla';
import { ChatStore } from '@/store/chat/store';
import { ChatAutoSuggestionAction, chatAutoSuggestion } from './autoSuggestion';
import { AIGenerateAction, generateAIChat } from './generateAIChat';
import { AIGenerateV2Action, generateAIChatV2 } from './generateAIChatV2';
import { ChatMemoryAction, chatMemory } from './memory';
@@ -11,7 +12,8 @@ export interface ChatAIChatAction
extends ChatRAGAction,
ChatMemoryAction,
AIGenerateAction,
AIGenerateV2Action {
AIGenerateV2Action,
ChatAutoSuggestionAction {
/**/
}
@@ -25,4 +27,5 @@ export const chatAiChat: StateCreator<
...generateAIChat(...params),
...chatMemory(...params),
...generateAIChatV2(...params),
...chatAutoSuggestion(...params),
});
+1
View File
@@ -16,6 +16,7 @@ export enum SidebarTabKey {
}
export enum ChatSettingsTabs {
AutoSuggestion = 'auto-suggestion',
Chat = 'chat',
Meta = 'meta',
Modal = 'modal',
@@ -15,9 +15,11 @@ const queryRewrite = (s: UserStore) => currentSystemAgent(s).queryRewrite;
const historyCompress = (s: UserStore) => currentSystemAgent(s).historyCompress;
const generationTopic = (s: UserStore) => currentSystemAgent(s).generationTopic;
const groupChatSupervisor = (s: UserStore) => currentSystemAgent(s).groupChatSupervisor;
const autoSuggestion = (s: UserStore) => currentSystemAgent(s).autoSuggestion;
export const systemAgentSelectors = {
agentMeta,
autoSuggestion,
generationTopic,
groupChatSupervisor,
historyCompress,