feat: support no-fc models like deepseek r1 with online search (#6842)

* update crawler rule

* feat: 完成联网集成

* update i18n

* update tests

* update tests

* fix tests

* improve performance

* fix error issue

* fix signal issue and improve implement

* fix pricing in CNY

* fix tests

* filter empty providers

* fix tests

* improve search crawler env

* fix search crawler env

* fix documents
This commit is contained in:
Arvin Xu
2025-03-10 02:35:52 +08:00
committed by GitHub
parent 23a26a9563
commit f284c25606
54 changed files with 665 additions and 170 deletions
@@ -36,7 +36,7 @@ tags:
<Image alt={'Clerk 添加 Webhooks 端点'} src={'https://github.com/lobehub/lobe-chat/assets/28616219/f50f47fb-5e8e-4930-bf4e-8cf6f5b8afb9'} />
在 endppint 中填写你的项目 URL,如 `https://your-project.com/api/webhooks/clerk`。然后在订阅事件(Subscribe to events)中,勾选 user 的三个事件(`user.created` 、`user.deleted`、`user.updated`),然后点击创建。
在 endpoint 中填写你的项目 URL,如 `https://your-project.com/api/webhooks/clerk`。然后在订阅事件(Subscribe to events)中,勾选 user 的三个事件(`user.created` 、`user.deleted`、`user.updated`),然后点击创建。
<Callout type={'warning'}>URL 的`https://`不可缺失,须保持 URL 的完整性</Callout>
@@ -140,7 +140,7 @@ tags:
<Image alt={'Clerk 添加 Webhooks 端点'} src={'https://github.com/lobehub/lobe-chat/assets/28616219/f50f47fb-5e8e-4930-bf4e-8cf6f5b8afb9'} />
在 endppint 中填写你的 Vercel 项目的 URL,如 `https://your-project.vercel.app/api/webhooks/clerk`。然后在订阅事件(Subscribe to events)中,勾选 user 的三个事件(`user.created` 、`user.deleted`、`user.updated`),然后点击创建。
在 endpoint 中填写你的 Vercel 项目的 URL,如 `https://your-project.vercel.app/api/webhooks/clerk`。然后在订阅事件(Subscribe to events)中,勾选 user 的三个事件(`user.created` 、`user.deleted`、`user.updated`),然后点击创建。
<Callout type={'warning'}>URL 的`https://`不可缺失,须保持 URL 的完整性</Callout>
+7 -1
View File
@@ -64,6 +64,9 @@
"stop": "توقف",
"warp": "تغيير السطر"
},
"intentUnderstanding": {
"title": "جارٍ تحليل وفهم نواياك..."
},
"knowledgeBase": {
"all": "جميع المحتويات",
"allFiles": "جميع الملفات",
@@ -144,7 +147,6 @@
"desc": "تحديد ما إذا كان من الضروري البحث بناءً على محتوى المحادثة",
"title": "الاتصال الذكي"
},
"disable": "النموذج الحالي لا يدعم استدعاء الوظائف، لذا لا يمكن استخدام وظيفة الاتصال الذكي",
"off": {
"desc": "استخدام المعرفة الأساسية للنموذج فقط، دون إجراء بحث عبر الإنترنت",
"title": "إيقاف الاتصال"
@@ -155,6 +157,10 @@
},
"useModelBuiltin": "استخدام محرك البحث المدمج في النموذج"
},
"searchModel": {
"desc": "النموذج الحالي لا يدعم استدعاء الدوال، لذا يجب استخدام نموذج يدعم استدعاء الدوال للبحث عبر الإنترنت",
"title": "نموذج البحث المساعد"
},
"title": "بحث عبر الإنترنت"
},
"searchAgentPlaceholder": "مساعد البحث...",
+7 -1
View File
@@ -64,6 +64,9 @@
"stop": "Спри",
"warp": "Нов ред"
},
"intentUnderstanding": {
"title": "Анализирам и разбирам вашето намерение..."
},
"knowledgeBase": {
"all": "Всички съдържания",
"allFiles": "Всички файлове",
@@ -144,7 +147,6 @@
"desc": "Интелигентно определяне на необходимостта от търсене въз основа на съдържанието на разговора",
"title": "Интелигентно свързване"
},
"disable": "Текущият модел не поддържа извикване на функции, затова не може да се използва интелигентно свързване",
"off": {
"desc": "Използва само основните знания на модела, без интернет търсене",
"title": "Изключване на свързването"
@@ -155,6 +157,10 @@
},
"useModelBuiltin": "Използване на вградената търсачка на модела"
},
"searchModel": {
"desc": "Текущият модел не поддържа извикване на функции, затова е необходимо да се комбинира с модел, който поддържа извикване на функции, за да се извърши търсене в интернет",
"title": "Модел за търсене на помощ"
},
"title": "Търсене в интернет"
},
"searchAgentPlaceholder": "Търсач на помощ...",
+7 -1
View File
@@ -64,6 +64,9 @@
"stop": "Stoppen",
"warp": "Zeilenumbruch"
},
"intentUnderstanding": {
"title": "Analysiere und verstehe Ihre Absicht..."
},
"knowledgeBase": {
"all": "Alle Inhalte",
"allFiles": "Alle Dateien",
@@ -144,7 +147,6 @@
"desc": "Intelligente Beurteilung, ob eine Suche basierend auf dem Gesprächsinhalt erforderlich ist",
"title": "Intelligente Vernetzung"
},
"disable": "Das aktuelle Modell unterstützt keine Funktionsaufrufe, daher kann die intelligente Vernetzungsfunktion nicht verwendet werden",
"off": {
"desc": "Verwendet nur das Grundwissen des Modells, ohne Netzsuche",
"title": "Vernetzung deaktivieren"
@@ -155,6 +157,10 @@
},
"useModelBuiltin": "Verwenden Sie die integrierte Suchmaschine des Modells"
},
"searchModel": {
"desc": "Das aktuelle Modell unterstützt keine Funktionsaufrufe, daher muss es mit einem Modell kombiniert werden, das Funktionsaufrufe unterstützt, um online zu suchen",
"title": "Suchunterstützungsmodell"
},
"title": "Netzwerksuche"
},
"searchAgentPlaceholder": "Suchassistent...",
+7 -1
View File
@@ -64,6 +64,9 @@
"stop": "Stop",
"warp": "New Line"
},
"intentUnderstanding": {
"title": "Analyzing and understanding your intent..."
},
"knowledgeBase": {
"all": "All Content",
"allFiles": "All Files",
@@ -144,7 +147,6 @@
"desc": "Intelligently determine whether a search is needed based on the conversation content",
"title": "Smart Online Search"
},
"disable": "The current model does not support function calls, so the smart online search feature is unavailable",
"off": {
"desc": "Use only the model's basic knowledge without performing a web search",
"title": "Disable Online Search"
@@ -155,6 +157,10 @@
},
"useModelBuiltin": "Use the model's built-in search engine"
},
"searchModel": {
"desc": "The current model does not support function calls, so it needs to be paired with a model that does support function calls for online searching.",
"title": "Search Assistant Model"
},
"title": "Online Search"
},
"searchAgentPlaceholder": "Search assistants...",
+7 -1
View File
@@ -64,6 +64,9 @@
"stop": "Detener",
"warp": "Salto de línea"
},
"intentUnderstanding": {
"title": "Analizando y comprendiendo su intención..."
},
"knowledgeBase": {
"all": "Todo el contenido",
"allFiles": "Todos los archivos",
@@ -144,7 +147,6 @@
"desc": "Determina inteligentemente si se necesita buscar según el contenido de la conversación",
"title": "Conexión inteligente"
},
"disable": "El modelo actual no admite llamadas a funciones, por lo que no se puede utilizar la función de conexión inteligente",
"off": {
"desc": "Utiliza solo el conocimiento básico del modelo, sin realizar búsquedas en línea",
"title": "Desactivar conexión"
@@ -155,6 +157,10 @@
},
"useModelBuiltin": "Utilizar el motor de búsqueda integrado del modelo"
},
"searchModel": {
"desc": "El modelo actual no admite llamadas a funciones, por lo que se necesita combinarlo con un modelo que admita llamadas a funciones para realizar búsquedas en línea",
"title": "Modelo de búsqueda auxiliar"
},
"title": "Búsqueda en línea"
},
"searchAgentPlaceholder": "Asistente de búsqueda...",
+7 -1
View File
@@ -64,6 +64,9 @@
"stop": "توقف",
"warp": "خط جدید"
},
"intentUnderstanding": {
"title": "در حال تحلیل و درک نیت شما..."
},
"knowledgeBase": {
"all": "همه محتوا",
"allFiles": "همه فایل‌ها",
@@ -144,7 +147,6 @@
"desc": "به طور هوشمندانه بر اساس محتوای گفتگو تشخیص می‌دهد که آیا نیاز به جستجو است",
"title": "اتصال هوشمند"
},
"disable": "مدل فعلی از فراخوانی توابع پشتیبانی نمی‌کند، بنابراین نمی‌توان از ویژگی اتصال هوشمند استفاده کرد",
"off": {
"desc": "فقط از دانش پایه مدل استفاده می‌کند و جستجوی اینترنتی انجام نمی‌دهد",
"title": "قطع اتصال"
@@ -155,6 +157,10 @@
},
"useModelBuiltin": "استفاده از موتور جستجوی داخلی مدل"
},
"searchModel": {
"desc": "مدل فعلی از فراخوانی توابع پشتیبانی نمی‌کند، بنابراین نیاز است که با مدلی که از فراخوانی توابع پشتیبانی می‌کند، برای جستجوی آنلاین ترکیب شود",
"title": "مدل جستجوی کمکی"
},
"title": "جستجوی متصل"
},
"searchAgentPlaceholder": "جستجوی دستیار...",
+7 -1
View File
@@ -64,6 +64,9 @@
"stop": "Arrêter",
"warp": "Saut de ligne"
},
"intentUnderstanding": {
"title": "Analyse et comprend votre intention..."
},
"knowledgeBase": {
"all": "Tout le contenu",
"allFiles": "Tous les fichiers",
@@ -144,7 +147,6 @@
"desc": "Détermine intelligemment si une recherche est nécessaire en fonction du contenu de la conversation",
"title": "Connexion intelligente"
},
"disable": "Le modèle actuel ne prend pas en charge l'appel de fonctions, donc la fonctionnalité de connexion intelligente est indisponible",
"off": {
"desc": "Utilise uniquement les connaissances de base du modèle, sans recherche en ligne",
"title": "Déconnexion"
@@ -155,6 +157,10 @@
},
"useModelBuiltin": "Utiliser le moteur de recherche intégré du modèle"
},
"searchModel": {
"desc": "Le modèle actuel ne prend pas en charge les appels de fonction, il doit donc être associé à un modèle prenant en charge les appels de fonction pour effectuer une recherche en ligne",
"title": "Modèle d'assistance à la recherche"
},
"title": "Recherche en ligne"
},
"searchAgentPlaceholder": "Assistant de recherche...",
+7 -1
View File
@@ -64,6 +64,9 @@
"stop": "Ferma",
"warp": "A capo"
},
"intentUnderstanding": {
"title": "Analizzando e comprendendo le tue intenzioni..."
},
"knowledgeBase": {
"all": "Tutti i contenuti",
"allFiles": "Tutti i file",
@@ -144,7 +147,6 @@
"desc": "Determina intelligentemente se è necessario cercare in base al contenuto della conversazione",
"title": "Collegamento intelligente"
},
"disable": "Il modello attuale non supporta le chiamate di funzione, quindi non è possibile utilizzare la funzionalità di collegamento intelligente",
"off": {
"desc": "Utilizza solo la conoscenza di base del modello, senza effettuare ricerche online",
"title": "Disattiva collegamento"
@@ -155,6 +157,10 @@
},
"useModelBuiltin": "Utilizza il motore di ricerca integrato del modello"
},
"searchModel": {
"desc": "Il modello attuale non supporta le chiamate di funzione, quindi è necessario utilizzarlo insieme a un modello che supporti le chiamate di funzione per cercare online",
"title": "Modello di ricerca assistita"
},
"title": "Ricerca online"
},
"searchAgentPlaceholder": "Assistente di ricerca...",
+7 -1
View File
@@ -64,6 +64,9 @@
"stop": "停止",
"warp": "改行"
},
"intentUnderstanding": {
"title": "あなたの意図を分析し理解しています..."
},
"knowledgeBase": {
"all": "すべてのコンテンツ",
"allFiles": "すべてのファイル",
@@ -144,7 +147,6 @@
"desc": "会話の内容に基づいて、検索が必要かどうかを自動的に判断します",
"title": "インテリジェント接続"
},
"disable": "現在のモデルは関数呼び出しをサポートしていないため、インテリジェント接続機能は使用できません",
"off": {
"desc": "モデルの基本知識のみを使用し、ネット検索は行いません",
"title": "接続をオフ"
@@ -155,6 +157,10 @@
},
"useModelBuiltin": "モデル内蔵の検索エンジンを使用"
},
"searchModel": {
"desc": "現在のモデルは関数呼び出しをサポートしていないため、関数呼び出しをサポートするモデルと組み合わせてネット検索を行う必要があります",
"title": "検索補助モデル"
},
"title": "ネット接続検索"
},
"searchAgentPlaceholder": "検索アシスタント...",
+7 -1
View File
@@ -64,6 +64,9 @@
"stop": "중지",
"warp": "줄바꿈"
},
"intentUnderstanding": {
"title": "귀하의 의도를 분석하고 이해하는 중입니다..."
},
"knowledgeBase": {
"all": "모든 내용",
"allFiles": "모든 파일",
@@ -144,7 +147,6 @@
"desc": "대화 내용을 기반으로 검색 필요성을 스마트하게 판단",
"title": "스마트 연결"
},
"disable": "현재 모델은 함수 호출을 지원하지 않으므로 스마트 연결 기능을 사용할 수 없습니다",
"off": {
"desc": "모델의 기본 지식만 사용하고 네트워크 검색을 수행하지 않음",
"title": "연결 끄기"
@@ -155,6 +157,10 @@
},
"useModelBuiltin": "모델 내장 검색 엔진 사용"
},
"searchModel": {
"desc": "현재 모델은 함수 호출을 지원하지 않으므로 함수 호출을 지원하는 모델과 함께 사용해야 인터넷 검색이 가능합니다.",
"title": "검색 보조 모델"
},
"title": "연결 검색"
},
"searchAgentPlaceholder": "검색 도우미...",
+7 -1
View File
@@ -64,6 +64,9 @@
"stop": "Stoppen",
"warp": "Nieuwe regel"
},
"intentUnderstanding": {
"title": "Bezig met het analyseren en begrijpen van uw intentie..."
},
"knowledgeBase": {
"all": "Alle inhoud",
"allFiles": "Alle bestanden",
@@ -144,7 +147,6 @@
"desc": "Intelligente beoordeling of er gezocht moet worden op basis van de gesprekinhoud",
"title": "Slimme verbinding"
},
"disable": "Het huidige model ondersteunt geen functieaanroepen, dus de slimme verbindingsfunctie kan niet worden gebruikt",
"off": {
"desc": "Gebruik alleen de basiskennis van het model, zonder online zoekopdrachten",
"title": "Verbinding uitschakelen"
@@ -155,6 +157,10 @@
},
"useModelBuiltin": "Gebruik de ingebouwde zoekmachine van het model"
},
"searchModel": {
"desc": "Het huidige model ondersteunt geen functieaanroepen, dus het moet worden gecombineerd met een model dat functieaanroepen ondersteunt om online te zoeken",
"title": "Zoekhulpmiddel model"
},
"title": "Online zoeken"
},
"searchAgentPlaceholder": "Zoekassistent...",
+7 -1
View File
@@ -64,6 +64,9 @@
"stop": "Zatrzymaj",
"warp": "Złamanie wiersza"
},
"intentUnderstanding": {
"title": "Analizuję i rozumiem Twoje intencje..."
},
"knowledgeBase": {
"all": "Wszystkie treści",
"allFiles": "Wszystkie pliki",
@@ -144,7 +147,6 @@
"desc": "Inteligentne określenie, czy potrzebne jest wyszukiwanie na podstawie treści rozmowy",
"title": "Inteligentne połączenie"
},
"disable": "Aktualny model nie obsługuje wywołań funkcji, więc nie można korzystać z inteligentnego połączenia",
"off": {
"desc": "Używaj tylko podstawowej wiedzy modelu, bez wyszukiwania w sieci",
"title": "Wyłącz połączenie"
@@ -155,6 +157,10 @@
},
"useModelBuiltin": "Użyj wbudowanej wyszukiwarki modelu"
},
"searchModel": {
"desc": "Aktualny model nie obsługuje wywołań funkcji, dlatego wymaga współpracy z modelem obsługującym wywołania funkcji, aby móc przeszukiwać sieć",
"title": "Model wspomagający wyszukiwanie"
},
"title": "Wyszukiwanie w sieci"
},
"searchAgentPlaceholder": "Wyszukaj pomocnika...",
+7 -1
View File
@@ -64,6 +64,9 @@
"stop": "Parar",
"warp": "Quebrar linha"
},
"intentUnderstanding": {
"title": "Analisando e compreendendo sua intenção..."
},
"knowledgeBase": {
"all": "Todo conteúdo",
"allFiles": "Todos os arquivos",
@@ -144,7 +147,6 @@
"desc": "Determina inteligentemente se é necessário pesquisar com base no conteúdo da conversa",
"title": "Conexão Inteligente"
},
"disable": "O modelo atual não suporta chamadas de função, portanto, a funcionalidade de conexão inteligente não está disponível",
"off": {
"desc": "Usa apenas o conhecimento básico do modelo, sem realizar pesquisas na web",
"title": "Desativar Conexão"
@@ -155,6 +157,10 @@
},
"useModelBuiltin": "Usar o mecanismo de busca embutido no modelo"
},
"searchModel": {
"desc": "O modelo atual não suporta chamadas de função, portanto, é necessário combiná-lo com um modelo que suporte chamadas de função para realizar buscas na internet",
"title": "Modelo de busca auxiliar"
},
"title": "Pesquisa Conectada"
},
"searchAgentPlaceholder": "Assistente de busca...",
+7 -1
View File
@@ -64,6 +64,9 @@
"stop": "Остановить",
"warp": "Перенос строки"
},
"intentUnderstanding": {
"title": "Анализ и понимание вашего намерения..."
},
"knowledgeBase": {
"all": "Все содержимое",
"allFiles": "Все файлы",
@@ -144,7 +147,6 @@
"desc": "Интеллектуально определяет необходимость поиска на основе содержания диалога",
"title": "Интеллектуальное подключение к сети"
},
"disable": "Текущая модель не поддерживает вызовы функций, поэтому функция интеллектуального подключения к сети недоступна",
"off": {
"desc": "Использует только базовые знания модели, без сетевого поиска",
"title": "Отключить подключение к сети"
@@ -155,6 +157,10 @@
},
"useModelBuiltin": "Использовать встроенный поисковый движок модели"
},
"searchModel": {
"desc": "Текущая модель не поддерживает вызов функций, поэтому необходимо использовать модель, поддерживающую вызов функций, для поиска в интернете",
"title": "Модель поиска"
},
"title": "Поиск в сети"
},
"searchAgentPlaceholder": "Поиск помощника...",
+7 -1
View File
@@ -64,6 +64,9 @@
"stop": "Dur",
"warp": "Satır atla"
},
"intentUnderstanding": {
"title": "Niyetinizi analiz ediyor ve anlıyor..."
},
"knowledgeBase": {
"all": "Tüm İçerik",
"allFiles": "Tüm Dosyalar",
@@ -144,7 +147,6 @@
"desc": "Sohbet içeriğine göre akıllıca arama gerekip gerekmediğini belirler",
"title": "Akıllı Bağlantı"
},
"disable": "Mevcut model fonksiyon çağrısını desteklemediği için akıllı bağlantı özelliği kullanılamaz",
"off": {
"desc": "Sadece modelin temel bilgilerini kullanır, ağ araması yapmaz",
"title": "Bağlantıyı Kapat"
@@ -155,6 +157,10 @@
},
"useModelBuiltin": "Modelin yerleşik arama motorunu kullan"
},
"searchModel": {
"desc": "Mevcut model fonksiyon çağrısını desteklemiyor, bu nedenle çevrimiçi arama yapmak için fonksiyon çağrısını destekleyen bir model ile birlikte kullanılması gerekiyor",
"title": "Arama Yardımcı Modeli"
},
"title": "Ağ Araması"
},
"searchAgentPlaceholder": "Arama Asistanı...",
+7 -1
View File
@@ -64,6 +64,9 @@
"stop": "Dừng",
"warp": "Xuống dòng"
},
"intentUnderstanding": {
"title": "Đang phân tích và hiểu ý định của bạn..."
},
"knowledgeBase": {
"all": "Tất cả nội dung",
"allFiles": "Tất cả tệp",
@@ -144,7 +147,6 @@
"desc": "Thông minh xác định xem có cần tìm kiếm dựa trên nội dung cuộc trò chuyện",
"title": "Kết nối thông minh"
},
"disable": "Mô hình hiện tại không hỗ trợ gọi hàm, do đó không thể sử dụng chức năng kết nối thông minh",
"off": {
"desc": "Chỉ sử dụng kiến thức cơ bản của mô hình, không thực hiện tìm kiếm trên mạng",
"title": "Tắt kết nối"
@@ -155,6 +157,10 @@
},
"useModelBuiltin": "Sử dụng công cụ tìm kiếm tích hợp của mô hình"
},
"searchModel": {
"desc": "Mô hình hiện tại không hỗ trợ gọi hàm, vì vậy cần kết hợp với mô hình hỗ trợ gọi hàm để tìm kiếm trực tuyến",
"title": "Mô hình hỗ trợ tìm kiếm"
},
"title": "Tìm kiếm trực tuyến"
},
"searchAgentPlaceholder": "Trợ lý tìm kiếm...",
+7 -1
View File
@@ -64,6 +64,9 @@
"stop": "停止",
"warp": "换行"
},
"intentUnderstanding": {
"title": "正在分析并理解意图您的意图..."
},
"knowledgeBase": {
"all": "所有内容",
"allFiles": "所有文件",
@@ -144,7 +147,6 @@
"desc": "根据对话内容智能判断是否需要搜索",
"title": "智能联网"
},
"disable": "当前模型不支持函数调用,因此无法使用智能联网功能",
"off": {
"desc": "仅使用模型的基础知识,不进行网络搜索",
"title": "关闭联网"
@@ -155,6 +157,10 @@
},
"useModelBuiltin": "使用模型内置搜索引擎"
},
"searchModel": {
"desc": "当前模型不支持函数调用,因此需要搭配支持函数调用的模型才能联网搜索",
"title": "搜索辅助模型"
},
"title": "联网搜索"
},
"searchAgentPlaceholder": "搜索助手...",
+7 -1
View File
@@ -64,6 +64,9 @@
"stop": "停止",
"warp": "換行"
},
"intentUnderstanding": {
"title": "正在分析並理解您的意圖..."
},
"knowledgeBase": {
"all": "所有內容",
"allFiles": "所有檔案",
@@ -144,7 +147,6 @@
"desc": "根據對話內容智能判斷是否需要搜尋",
"title": "智能連網"
},
"disable": "當前模型不支持函數調用,因此無法使用智能連網功能",
"off": {
"desc": "僅使用模型的基礎知識,不進行網路搜尋",
"title": "關閉連網"
@@ -155,6 +157,10 @@
},
"useModelBuiltin": "使用模型內建搜尋引擎"
},
"searchModel": {
"desc": "當前模型不支持函數調用,因此需要搭配支持函數調用的模型才能聯網搜索",
"title": "搜索輔助模型"
},
"title": "連網搜尋"
},
"searchAgentPlaceholder": "搜尋助手...",
+11 -1
View File
@@ -3,8 +3,18 @@ import { CrawlUrlRule } from './type';
import { crawUrlRules } from './urlRules';
import { applyUrlRules } from './utils/appUrlRules';
interface CrawlOptions {
impls?: string[];
}
export class Crawler {
impls = ['naive', 'jina', 'browserless'] as const;
impls: CrawlImplType[];
constructor(options: CrawlOptions = {}) {
this.impls = !!options.impls?.length
? (options.impls.filter((impl) => Object.keys(crawlImpls).includes(impl)) as CrawlImplType[])
: (['naive', 'jina', 'browserless'] as const);
}
/**
* 爬取网页内容
@@ -181,7 +181,10 @@ describe('POST handler', () => {
const response = await POST(request as unknown as Request, { params: mockParams });
expect(response).toEqual(mockChatResponse);
expect(AgentRuntime.prototype.chat).toHaveBeenCalledWith(mockChatPayload, { user: 'abc' });
expect(AgentRuntime.prototype.chat).toHaveBeenCalledWith(mockChatPayload, {
user: 'abc',
signal: expect.anything(),
});
});
it('should return an error response when chat completion fails', async () => {
@@ -39,7 +39,11 @@ export const POST = checkAuth(async (req: Request, { params, jwtPayload, createR
});
}
return await agentRuntime.chat(data, { user: jwtPayload.userId, ...traceOptions });
return await agentRuntime.chat(data, {
user: jwtPayload.userId,
...traceOptions,
signal: req.signal,
});
} catch (e) {
const {
errorType = ChatErrorType.InternalServerError,
@@ -1,4 +1,5 @@
import { createStyles } from 'antd-style';
import { memo } from 'react';
export const useStyles = createStyles(({ css, token }, borderWidth: number = 2.5) => ({
background: css`
@@ -44,7 +45,7 @@ export const useStyles = createStyles(({ css, token }, borderWidth: number = 2.5
`,
}));
const Loader = () => {
const CircleLoader = memo(() => {
const { styles } = useStyles();
return (
@@ -53,6 +54,6 @@ const Loader = () => {
<div className={styles.background} />
</div>
);
};
});
export default Loader;
export default CircleLoader;
+2
View File
@@ -4,10 +4,12 @@ import { z } from 'zod';
export const getToolsConfig = () => {
return createEnv({
runtimeEnv: {
CRAWLER_IMPLS: process.env.CRAWLER_IMPLS,
SEARXNG_URL: process.env.SEARXNG_URL,
},
server: {
CRAWLER_IMPLS: z.string().optional(),
SEARXNG_URL: z.string().url().optional(),
},
});
+6
View File
@@ -13,6 +13,11 @@ export const DEFAUTT_AGENT_TTS_CONFIG: LobeAgentTTSConfig = {
},
};
export const DEFAULT_AGENT_SEARCH_FC_MODEL = {
model: DEFAULT_MODEL,
provider: ModelProvider.OpenAI,
};
export const DEFAULT_AGENT_CHAT_CONFIG: LobeAgentChatConfig = {
autoCreateTopicThreshold: 2,
displayMode: 'chat',
@@ -22,6 +27,7 @@ export const DEFAULT_AGENT_CHAT_CONFIG: LobeAgentChatConfig = {
enableReasoning: false,
historyCount: 8,
reasoningBudgetToken: 1024,
searchFCModel: DEFAULT_AGENT_SEARCH_FC_MODEL,
searchMode: 'off',
};
@@ -0,0 +1,56 @@
import { createStyles } from 'antd-style';
import { useTranslation } from 'react-i18next';
import { Flexbox } from 'react-layout-kit';
import InfoTooltip from '@/components/InfoTooltip';
import { useAgentStore } from '@/store/agent';
import { agentChatConfigSelectors } from '@/store/agent/slices/chat';
import FunctionCallingModelSelect from './FunctionCallingModelSelect';
const useStyles = createStyles(({ css, token }) => ({
check: css`
margin-inline-start: 12px;
font-size: 16px;
color: ${token.colorPrimary};
`,
content: css`
flex: 1;
width: 230px;
`,
description: css`
width: 200px;
font-size: 12px;
color: ${token.colorTextSecondary};
`,
title: css`
font-size: 14px;
font-weight: 500;
color: ${token.colorText};
`,
}));
const FCSearchModel = () => {
const { t } = useTranslation('chat');
const { styles } = useStyles();
const [searchFCModel, updateAgentChatConfig] = useAgentStore((s) => [
agentChatConfigSelectors.searchFCModel(s),
s.updateAgentChatConfig,
]);
return (
<Flexbox distribution={'space-between'} gap={16} horizontal padding={8}>
<Flexbox align={'center'} gap={4} horizontal>
<Flexbox className={styles.title}>{t('search.searchModel.title')}</Flexbox>
<InfoTooltip title={t('search.searchModel.desc')} />
</Flexbox>
<FunctionCallingModelSelect
onChange={(value) => {
updateAgentChatConfig({ searchFCModel: value });
}}
value={searchFCModel}
/>
</Flexbox>
);
};
export default FCSearchModel;
@@ -0,0 +1,85 @@
import { Select, SelectProps } from 'antd';
import { createStyles } from 'antd-style';
import { memo, useMemo } from 'react';
import { ModelItemRender, ProviderItemRender } from '@/components/ModelSelect';
import { useEnabledChatModels } from '@/hooks/useEnabledChatModels';
import { WorkingModel } from '@/types/agent';
import { EnabledProviderWithModels } from '@/types/aiProvider';
const useStyles = createStyles(({ css, prefixCls }) => ({
select: css`
&.${prefixCls}-select-dropdown .${prefixCls}-select-item-option-grouped {
padding-inline-start: 12px;
}
`,
}));
interface ModelOption {
label: any;
provider: string;
value: string;
}
interface ModelSelectProps {
onChange?: (props: WorkingModel) => void;
showAbility?: boolean;
value?: WorkingModel;
}
const ModelSelect = memo<ModelSelectProps>(({ value, onChange }) => {
const enabledList = useEnabledChatModels();
const { styles } = useStyles();
const options = useMemo<SelectProps['options']>(() => {
const getChatModels = (provider: EnabledProviderWithModels) =>
provider.children
.filter((model) => !!model.abilities.functionCall)
.map((model) => ({
label: <ModelItemRender {...model} {...model.abilities} showInfoTag={false} />,
provider: provider.id,
value: `${provider.id}/${model.id}`,
}));
if (enabledList.length === 1) {
const provider = enabledList[0];
return getChatModels(provider);
}
return enabledList
.filter((p) => !!getChatModels(p).length)
.map((provider) => {
const options = getChatModels(provider);
return {
label: (
<ProviderItemRender
logo={provider.logo}
name={provider.name}
provider={provider.id}
source={provider.source}
/>
),
options,
};
});
}, [enabledList]);
return (
<Select
onChange={(value, option) => {
const model = value.split('/').slice(1).join('/');
onChange?.({ model, provider: (option as unknown as ModelOption).provider });
}}
options={options}
popupClassName={styles.select}
popupMatchSelectWidth={false}
value={`${value?.provider}/${value?.model}`}
variant={'filled'}
/>
);
});
export default ModelSelect;
@@ -12,6 +12,7 @@ import { agentChatConfigSelectors, agentSelectors } from '@/store/agent/slices/c
import { aiModelSelectors, useAiInfraStore } from '@/store/aiInfra';
import { SearchMode } from '@/types/search';
import FCSearchModel from './FCSearchModel';
import ModelBuiltinSearch from './ModelBuiltinSearch';
const { Text } = Typography;
@@ -38,10 +39,6 @@ const useStyles = createStyles(({ css, token }) => ({
font-size: 12px;
color: ${token.colorTextSecondary};
`,
disable: css`
cursor: not-allowed;
opacity: 0.45;
`,
iconWrapper: css`
display: flex;
flex-shrink: 0;
@@ -80,8 +77,7 @@ const useStyles = createStyles(({ css, token }) => ({
`,
}));
const Item = memo<NetworkOption>(({ value, description, icon, label, disable }) => {
const { t } = useTranslation('chat');
const Item = memo<NetworkOption>(({ value, description, icon, label }) => {
const { styles } = useStyles();
const [mode, updateAgentChatConfig] = useAgentStore((s) => [
agentChatConfigSelectors.agentSearchMode(s),
@@ -96,12 +92,12 @@ const Item = memo<NetworkOption>(({ value, description, icon, label, disable })
key={value}
onClick={() => updateAgentChatConfig({ searchMode: value })}
>
<Flexbox className={disable ? styles.disable : ''} gap={8} horizontal>
<Flexbox gap={8} horizontal>
<div className={styles.iconWrapper}>{icon}</div>
<div className={styles.content}>
<div className={styles.title}>{label}</div>
<Text className={styles.description} type="secondary">
{disable ? t('search.mode.disable') : description}
{description}
</Text>
</div>
</Flexbox>
@@ -132,34 +128,24 @@ const AINetworkSettings = memo<AINetworkSettingsProps>(() => {
label: t('search.mode.off.title'),
value: 'off',
},
// 等应用层联网功能做好以后再开启
// {
// description: t('search.mode.on.desc'),
// icon: <WifiOutlined />,
// label: t('search.mode.on.title'),
// value: 'on',
// },
{
description: t('search.mode.auto.desc'),
disable: !supportFC,
icon: <Icon icon={SparklesIcon} />,
label: t('search.mode.auto.title'),
value: 'auto',
},
];
const showDivider = isModelHasBuiltinSearchConfig || !supportFC;
return (
<Flexbox gap={8}>
{options.map((option) => (
<Item {...option} key={option.value} />
))}
{isModelHasBuiltinSearchConfig && (
<>
<Divider style={{ margin: 0, paddingInline: 12 }} />
<ModelBuiltinSearch />
</>
)}
{showDivider && <Divider style={{ margin: 0, paddingInline: 12 }} />}
{isModelHasBuiltinSearchConfig && <ModelBuiltinSearch />}
{!supportFC && <FCSearchModel />}
</Flexbox>
);
});
@@ -7,11 +7,10 @@ import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { Flexbox } from 'react-layout-kit';
import { getPrice } from '@/features/Conversation/Extras/Usage/UsageDetail/pricing';
import { useGlobalStore } from '@/store/global';
import { systemStatusSelectors } from '@/store/global/selectors';
import { LobeDefaultAiModelListItem } from '@/types/aiModel';
import { ModelPriceCurrency } from '@/types/llm';
import { formatPriceByCurrency } from '@/utils/format';
export const useStyles = createStyles(({ css, token }) => {
return {
@@ -40,19 +39,8 @@ const ModelCard = memo<ModelCardProps>(({ pricing, id, provider, displayName })
const isShowCredit = useGlobalStore(systemStatusSelectors.isShowCredit) && !!pricing;
const updateSystemStatus = useGlobalStore((s) => s.updateSystemStatus);
const inputPrice = formatPriceByCurrency(pricing?.input, pricing?.currency as ModelPriceCurrency);
const cachedInputPrice = formatPriceByCurrency(
pricing?.cachedInput,
pricing?.currency as ModelPriceCurrency,
);
const writeCacheInputPrice = formatPriceByCurrency(
pricing?.writeCacheInput,
pricing?.currency as ModelPriceCurrency,
);
const outputPrice = formatPriceByCurrency(
pricing?.output,
pricing?.currency as ModelPriceCurrency,
);
const formatPrice = getPrice(pricing || {});
return (
<Flexbox gap={8}>
<Flexbox
@@ -103,37 +91,41 @@ const ModelCard = memo<ModelCardProps>(({ pricing, id, provider, displayName })
{pricing?.cachedInput && (
<Tooltip
title={t('messages.modelCard.pricing.inputCachedTokens', {
amount: cachedInputPrice,
amount: formatPrice.cachedInput,
})}
>
<Flexbox gap={2} horizontal>
<Icon icon={CircleFadingArrowUp} />
{cachedInputPrice}
{formatPrice.cachedInput}
</Flexbox>
</Tooltip>
)}
{pricing?.writeCacheInput && (
<Tooltip
title={t('messages.modelCard.pricing.writeCacheInputTokens', {
amount: writeCacheInputPrice,
amount: formatPrice.writeCacheInput,
})}
>
<Flexbox gap={2} horizontal>
<Icon icon={BookUp2Icon} />
{writeCacheInputPrice}
{formatPrice.writeCacheInput}
</Flexbox>
</Tooltip>
)}
<Tooltip title={t('messages.modelCard.pricing.inputTokens', { amount: inputPrice })}>
<Tooltip
title={t('messages.modelCard.pricing.inputTokens', { amount: formatPrice.input })}
>
<Flexbox gap={2} horizontal>
<Icon icon={ArrowUpFromDot} />
{inputPrice}
{formatPrice.input}
</Flexbox>
</Tooltip>
<Tooltip title={t('messages.modelCard.pricing.outputTokens', { amount: outputPrice })}>
<Tooltip
title={t('messages.modelCard.pricing.outputTokens', { amount: formatPrice.output })}
>
<Flexbox gap={2} horizontal>
<Icon icon={ArrowDownToDot} />
{outputPrice}
{formatPrice.output}
</Flexbox>
</Tooltip>
</Flexbox>
@@ -0,0 +1,26 @@
import { ChatModelPricing } from '@/types/aiModel';
import { ModelPriceCurrency } from '@/types/llm';
import { formatPriceByCurrency } from '@/utils/format';
export const getPrice = (pricing: ChatModelPricing) => {
const inputPrice = formatPriceByCurrency(pricing?.input, pricing?.currency as ModelPriceCurrency);
const cachedInputPrice = formatPriceByCurrency(
pricing?.cachedInput,
pricing?.currency as ModelPriceCurrency,
);
const writeCacheInputPrice = formatPriceByCurrency(
pricing?.writeCacheInput,
pricing?.currency as ModelPriceCurrency,
);
const outputPrice = formatPriceByCurrency(
pricing?.output,
pricing?.currency as ModelPriceCurrency,
);
return {
cachedInput: Number(cachedInputPrice),
input: Number(inputPrice),
output: Number(outputPrice),
writeCacheInput: Number(writeCacheInputPrice),
};
};
@@ -70,7 +70,7 @@ describe('getDetailsToken', () => {
const result = getDetailsToken(usage, mockModelCard);
expect(result.inputCached).toEqual({
credit: 0, // 50 * 0.005 = 0.25, rounded to 0
credit: 1,
token: 50,
});
@@ -165,12 +165,12 @@ describe('getDetailsToken', () => {
const result = getDetailsToken(usage, mockModelCard);
// uncachedInput: (200 - 50) * 0.01 = 1.5 -> 2
// cachedInput: 50 * 0.005 = 0.25 -> 0
// cachedInput: 50 * 0.005 = 0.25 -> 1
// totalOutput: 300 * 0.02 = 6
// totalCredit = 2 + 0 + 6 = 8
// totalCredit = 2 + 1 + 6 = 9
expect(result.totalTokens).toEqual({
credit: 8,
credit: 9,
token: 500,
});
});
@@ -1,6 +1,8 @@
import { LobeDefaultAiModelListItem } from '@/types/aiModel';
import { ChatModelPricing, LobeDefaultAiModelListItem } from '@/types/aiModel';
import { ModelTokensUsage } from '@/types/message';
import { getPrice } from './pricing';
const calcCredit = (token: number, pricing?: number) => {
if (!pricing) return '-';
@@ -29,23 +31,26 @@ export const getDetailsToken = (
? usage?.inputCacheMissTokens
: totalInputTokens - (inputCacheTokens || 0);
// Pricing
const formatPrice = getPrice(modelCard?.pricing as ChatModelPricing);
const inputCacheMissCredit = (
!!inputCacheMissTokens ? calcCredit(inputCacheMissTokens, modelCard?.pricing?.input) : 0
!!inputCacheMissTokens ? calcCredit(inputCacheMissTokens, formatPrice.input) : 0
) as number;
const inputCachedCredit = (
!!inputCacheTokens ? calcCredit(inputCacheTokens, modelCard?.pricing?.cachedInput) : 0
!!inputCacheTokens ? calcCredit(inputCacheTokens, formatPrice.cachedInput) : 0
) as number;
const inputWriteCachedCredit = !!inputWriteCacheTokens
? (calcCredit(inputWriteCacheTokens, modelCard?.pricing?.writeCacheInput) as number)
? (calcCredit(inputWriteCacheTokens, formatPrice.writeCacheInput) as number)
: 0;
const totalOutputCredit = (
!!totalOutputTokens ? calcCredit(totalOutputTokens, modelCard?.pricing?.output) : 0
!!totalOutputTokens ? calcCredit(totalOutputTokens, formatPrice.output) : 0
) as number;
const totalInputCredit = (
!!totalInputTokens ? calcCredit(totalInputTokens, modelCard?.pricing?.output) : 0
!!totalInputTokens ? calcCredit(totalInputTokens, formatPrice.output) : 0
) as number;
const totalCredit =
@@ -69,13 +74,13 @@ export const getDetailsToken = (
: undefined,
inputCitation: !!usage.inputCitationTokens
? {
credit: calcCredit(usage.inputCitationTokens, modelCard?.pricing?.input),
credit: calcCredit(usage.inputCitationTokens, formatPrice.input),
token: usage.inputCitationTokens,
}
: undefined,
inputText: !!inputTextTokens
? {
credit: calcCredit(inputTextTokens, modelCard?.pricing?.input),
credit: calcCredit(inputTextTokens, formatPrice.input),
token: inputTextTokens,
}
: undefined,
@@ -89,13 +94,13 @@ export const getDetailsToken = (
: undefined,
outputReasoning: !!outputReasoningTokens
? {
credit: calcCredit(outputReasoningTokens, modelCard?.pricing?.output),
credit: calcCredit(outputReasoningTokens, formatPrice.output),
token: outputReasoningTokens,
}
: undefined,
outputText: !!outputTextTokens
? {
credit: calcCredit(outputTextTokens, modelCard?.pricing?.output),
credit: calcCredit(outputTextTokens, formatPrice.output),
token: outputTextTokens,
}
: undefined,
@@ -0,0 +1,25 @@
import { createStyles } from 'antd-style';
import { useTranslation } from 'react-i18next';
import { Flexbox } from 'react-layout-kit';
import CircleLoader from '@/components/CircleLoader';
import { shinyTextStylish } from '@/styles/loading';
const useStyles = createStyles(({ token }) => ({
shinyText: shinyTextStylish(token),
}));
const IntentUnderstanding = () => {
const { styles } = useStyles();
const { t } = useTranslation('chat');
return (
<Flexbox align={'center'} gap={8} horizontal>
<CircleLoader />
<Flexbox className={styles.shinyText} horizontal>
{t('intentUnderstanding.title')}
</Flexbox>
</Flexbox>
);
};
export default IntentUnderstanding;
@@ -6,6 +6,7 @@ import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { Flexbox } from 'react-layout-kit';
import Loader from '@/components/CircleLoader';
import PluginAvatar from '@/features/PluginAvatar';
import { useChatStore } from '@/store/chat';
import { chatSelectors } from '@/store/chat/selectors';
@@ -14,8 +15,6 @@ import { toolSelectors } from '@/store/tool/selectors';
import { shinyTextStylish } from '@/styles/loading';
import { WebBrowsingManifest } from '@/tools/web-browsing';
import Loader from './Loader';
export const useStyles = createStyles(({ css, token }) => ({
apiName: css`
overflow: hidden;
@@ -8,6 +8,7 @@ import { ChatMessage } from '@/types/message';
import { DefaultMessage } from '../Default';
import FileChunks from './FileChunks';
import IntentUnderstanding from './IntentUnderstanding';
import Reasoning from './Reasoning';
import SearchGrounding from './SearchGrounding';
import Tool from './Tool';
@@ -24,6 +25,8 @@ export const AssistantMessage = memo<
const isReasoning = useChatStore(aiChatSelectors.isMessageInReasoning(id));
const isIntentUnderstanding = useChatStore(aiChatSelectors.isIntentUnderstanding(id));
const showSearch = !!search && !!search.citations?.length;
// remove \n to avoid empty content
@@ -32,6 +35,8 @@ export const AssistantMessage = memo<
(!!props.reasoning && props.reasoning.content?.trim() !== '') ||
(!props.reasoning && isReasoning);
const showFileChunks = !!chunksList && chunksList.length > 0;
return editing ? (
<DefaultMessage
content={content}
@@ -44,16 +49,20 @@ export const AssistantMessage = memo<
{showSearch && (
<SearchGrounding citations={search?.citations} searchQueries={search?.searchQueries} />
)}
{!!chunksList && chunksList.length > 0 && <FileChunks data={chunksList} />}
{showFileChunks && <FileChunks data={chunksList} />}
{showReasoning && <Reasoning {...props.reasoning} id={id} />}
{content && (
<DefaultMessage
addIdOnDOM={false}
content={content}
id={id}
isToolCallGenerating={isToolCallGenerating}
{...props}
/>
{isIntentUnderstanding ? (
<IntentUnderstanding />
) : (
content && (
<DefaultMessage
addIdOnDOM={false}
content={content}
id={id}
isToolCallGenerating={isToolCallGenerating}
{...props}
/>
)
)}
{tools && (
<Flexbox gap={8}>
+1 -4
View File
@@ -115,10 +115,7 @@ const ModelSwitchPanel = memo<PropsWithChildren>(({ children }) => {
provider={provider.id}
source={provider.source}
/>
<Link
href={isDeprecatedEdition ? '/settings/llm' : `/settings/provider/${provider.id}`}
prefetch={false}
>
<Link href={isDeprecatedEdition ? '/settings/llm' : `/settings/provider/${provider.id}`}>
<ActionIcon
icon={LucideBolt}
size={'small'}
+1 -9
View File
@@ -9,19 +9,11 @@ export const useAgentEnableSearch = () => {
agentChatConfigSelectors.agentSearchMode(s),
]);
const isModelSupportToolUse = useAiInfraStore(
aiModelSelectors.isModelSupportToolUse(model, provider),
);
const searchImpl = useAiInfraStore(aiModelSelectors.modelBuiltinSearchImpl(model, provider));
// 只要是内置的搜索实现,一定可以联网搜索
if (searchImpl === 'internal') return true;
// 如果是关闭状态,一定不能联网搜索
if (agentSearchMode === 'off') return false;
// 如果是智能模式,根据是否支持 Tool Calling 判断
if (agentSearchMode === 'auto') {
return isModelSupportToolUse;
}
return agentSearchMode !== 'off';
};
+7 -3
View File
@@ -65,6 +65,9 @@ export default {
stop: '停止',
warp: '换行',
},
intentUnderstanding: {
title: '正在分析并理解意图您的意图...',
},
knowledgeBase: {
all: '所有内容',
allFiles: '所有文件',
@@ -142,13 +145,11 @@ export default {
searchQueries: '搜索关键词',
title: '已搜索到 {{count}} 个结果',
},
mode: {
auto: {
desc: '根据对话内容智能判断是否需要搜索',
title: '智能联网',
},
disable: '当前模型不支持函数调用,因此无法使用智能联网功能',
off: {
desc: '仅使用模型的基础知识,不进行网络搜索',
title: '关闭联网',
@@ -159,7 +160,10 @@ export default {
},
useModelBuiltin: '使用模型内置搜索引擎',
},
searchModel: {
desc: '当前模型不支持函数调用,因此需要搭配支持函数调用的模型才能联网搜索',
title: '搜索辅助模型',
},
title: '联网搜索',
},
searchAgentPlaceholder: '搜索助手...',
+8 -1
View File
@@ -20,7 +20,14 @@ export const searchRouter = router({
}),
)
.mutation(async ({ input }) => {
const crawler = new Crawler();
const envString = toolsEnv.CRAWLER_IMPLS || '';
// 处理全角逗号和多余空格
let envValue = envString.replaceAll('', ',').trim();
const impls = envValue.split(',').filter(Boolean);
const crawler = new Crawler({ impls });
const results = await pMap(
input.urls,
+1
View File
@@ -877,6 +877,7 @@ describe('ChatService', () => {
// 重新模拟模块,设置 isServerMode 为 true
vi.doMock('@/const/version', () => ({
isServerMode: true,
isDeprecatedEdition: false,
}));
// 需要在修改模拟后重新导入相关模块
+63 -30
View File
@@ -32,6 +32,7 @@ import {
userProfileSelectors,
} from '@/store/user/selectors';
import { WebBrowsingManifest } from '@/tools/web-browsing';
import { WorkingModel } from '@/types/agent';
import { ChatErrorType } from '@/types/fetch';
import { ChatMessage, MessageToolCall } from '@/types/message';
import type { ChatStreamPayload, OpenAIChatMessage } from '@/types/openai/chat';
@@ -201,17 +202,10 @@ class ChatService {
// ============ 2. preprocess tools ============ //
let filterTools = toolSelectors.enabledSchema(pluginIds)(getToolStoreState());
// check this model can use function call
const canUseFC = isCanUseFC(payload.model, payload.provider!);
// the rule that model can use tools:
// 1. tools is not empty
// 2. model can use function call
const shouldUseTools = filterTools.length > 0 && canUseFC;
const tools = shouldUseTools ? filterTools : undefined;
const tools = this.prepareTools(pluginIds, {
model: payload.model,
provider: payload.provider!,
});
// ============ 3. process extend params ============ //
@@ -433,15 +427,29 @@ class ChatService {
onLoadingChange?.(true);
try {
await this.getChatCompletion(params, {
onErrorHandle: (error) => {
errorHandle(new Error(error.message), error);
},
onFinish,
onMessageHandle,
signal: abortController?.signal,
trace: this.mapTrace(trace, TraceTagMap.SystemChain),
const oaiMessages = this.processMessages({
messages: params.messages as any,
model: params.model!,
provider: params.provider!,
tools: params.plugins,
});
const tools = this.prepareTools(params.plugins || [], {
model: params.model!,
provider: params.provider!,
});
await this.getChatCompletion(
{ ...params, messages: oaiMessages, tools },
{
onErrorHandle: (error) => {
errorHandle(new Error(error.message), error);
},
onFinish,
onMessageHandle,
signal: abortController?.signal,
trace: this.mapTrace(trace, TraceTagMap.SystemChain),
},
);
onLoadingChange?.(false);
} catch (e) {
@@ -451,7 +459,7 @@ class ChatService {
private processMessages = (
{
messages,
messages = [],
tools,
model,
provider,
@@ -483,6 +491,7 @@ class ChatService {
};
let postMessages = messages.map((m): OpenAIChatMessage => {
const supportTools = isCanUseFC(model, provider);
switch (m.role) {
case 'user': {
return { content: getContent(m), role: m.role };
@@ -492,17 +501,23 @@ class ChatService {
// signature is a signal of anthropic thinking mode
const shouldIncludeThinking = m.reasoning && !!m.reasoning?.signature;
const content = shouldIncludeThinking
? [
{
signature: m.reasoning!.signature,
thinking: m.reasoning!.content,
type: 'thinking',
} as any,
{ text: m.content, type: 'text' },
]
: m.content;
if (!supportTools) {
return { content, role: m.role };
}
return {
content: shouldIncludeThinking
? [
{
signature: m.reasoning!.signature,
thinking: m.reasoning!.content,
type: 'thinking',
} as any,
{ text: m.content, type: 'text' },
]
: m.content,
content,
role: m.role,
tool_calls: m.tools?.map(
(tool): MessageToolCall => ({
@@ -518,6 +533,10 @@ class ChatService {
}
case 'tool': {
if (!supportTools) {
return { content: m.content, role: 'user' };
}
return {
content: m.content,
name: genToolCallingName(m.plugin!.identifier, m.plugin!.apiName, m.plugin?.type),
@@ -669,6 +688,20 @@ class ChatService {
return reorderedMessages;
};
private prepareTools = (pluginIds: string[], { model, provider }: WorkingModel) => {
let filterTools = toolSelectors.enabledSchema(pluginIds)(getToolStoreState());
// check this model can use function call
const canUseFC = isCanUseFC(model, provider!);
// the rule that model can use tools:
// 1. tools is not empty
// 2. model can use function call
const shouldUseTools = filterTools.length > 0 && canUseFC;
return shouldUseTools ? filterTools : undefined;
};
}
export const chatService = new ChatService();
+1 -1
View File
@@ -53,7 +53,7 @@ export interface ISessionService {
updateSessionChatConfig(
id: string,
config: DeepPartial<LobeAgentChatConfig>,
config: Partial<LobeAgentChatConfig>,
signal?: AbortSignal,
): Promise<any>;
@@ -11,6 +11,10 @@ exports[`agentSelectors > defaultAgentConfig > should merge DEFAULT_AGENT_CONFIG
"enableReasoning": false,
"historyCount": 8,
"reasoningBudgetToken": 1024,
"searchFCModel": {
"model": "gpt-4o-mini",
"provider": "openai",
},
"searchMode": "off",
},
"model": "gpt-3.5-turbo",
@@ -1,5 +1,5 @@
import { contextCachingModels, thinkingWithToolClaudeModels } from '@/const/models';
import { DEFAULT_AGENT_CHAT_CONFIG } from '@/const/settings';
import { DEFAULT_AGENT_CHAT_CONFIG, DEFAULT_AGENT_SEARCH_FC_MODEL } from '@/const/settings';
import { AgentStoreState } from '@/store/agent/initialState';
import { LobeAgentChatConfig } from '@/types/agent';
@@ -14,6 +14,9 @@ const isAgentEnableSearch = (s: AgentStoreState) => agentSearchMode(s) !== 'off'
const useModelBuiltinSearch = (s: AgentStoreState) =>
currentAgentChatConfig(s).useModelBuiltinSearch;
const searchFCModel = (s: AgentStoreState) =>
currentAgentChatConfig(s).searchFCModel || DEFAULT_AGENT_SEARCH_FC_MODEL;
const enableHistoryCount = (s: AgentStoreState) => {
const config = currentAgentConfig(s);
const chatConfig = currentAgentChatConfig(s);
@@ -62,5 +65,6 @@ export const agentChatConfigSelectors = {
enableHistoryDivider,
historyCount,
isAgentEnableSearch,
searchFCModel,
useModelBuiltinSearch,
};
@@ -765,10 +765,12 @@ describe('chatMessage actions', () => {
(fetch as Mock).mockResolvedValueOnce(new Response(aiResponse));
await act(async () => {
const response = await result.current.internal_fetchAIChatMessage(
const response = await result.current.internal_fetchAIChatMessage({
messages,
assistantMessageId,
);
messageId: assistantMessageId,
model: 'gpt-4o-mini',
provider: 'openai',
});
expect(response.isFunctionCall).toEqual(false);
});
});
@@ -784,7 +786,13 @@ describe('chatMessage actions', () => {
await act(async () => {
expect(
await result.current.internal_fetchAIChatMessage(messages, assistantMessageId),
await result.current.internal_fetchAIChatMessage({
model: 'gpt-4o-mini',
provider: 'openai',
messages,
messageId: assistantMessageId,
}),
).toEqual({
isFunctionCall: false,
});
@@ -13,10 +13,13 @@ import { messageService } from '@/services/message';
import { useAgentStore } from '@/store/agent';
import { agentChatConfigSelectors, agentSelectors } from '@/store/agent/selectors';
import { getAgentStoreState } from '@/store/agent/store';
import { aiModelSelectors } from '@/store/aiInfra';
import { getAiInfraStoreState } from '@/store/aiInfra/store';
import { chatHelpers } from '@/store/chat/helpers';
import { ChatStore } from '@/store/chat/store';
import { messageMapKey } from '@/store/chat/utils/messageMapKey';
import { useSessionStore } from '@/store/session';
import { WebBrowsingManifest } from '@/tools/web-browsing';
import { ChatMessage, CreateMessageParams, SendMessageParams } from '@/types/message';
import { MessageSemanticSearchChunk } from '@/types/rag';
import { setNamespace } from '@/utils/storeDebug';
@@ -28,6 +31,7 @@ const n = setNamespace('ai');
interface ProcessMessageParams {
traceId?: string;
isWelcomeQuestion?: boolean;
inSearchWorkflow?: boolean;
/**
* the RAG query content, should be embedding and used in the semantic search
*/
@@ -70,11 +74,13 @@ export interface AIGenerateAction {
/**
* Retrieves an AI-generated chat message from the backend service
*/
internal_fetchAIChatMessage: (
messages: ChatMessage[],
messageId: string,
params?: ProcessMessageParams,
) => Promise<{
internal_fetchAIChatMessage: (input: {
messages: ChatMessage[];
messageId: string;
params?: ProcessMessageParams;
model: string;
provider: string;
}) => Promise<{
isFunctionCall: boolean;
traceId?: string;
}>;
@@ -110,6 +116,8 @@ export interface AIGenerateAction {
id?: string,
action?: string,
) => AbortController | undefined;
internal_toggleSearchWorkflow: (loading: boolean, id?: string) => void;
}
export const generateAIChat: StateCreator<
@@ -336,10 +344,97 @@ export const generateAIChat: StateCreator<
const assistantId = await get().internal_createMessage(assistantMessage);
// 3. fetch the AI response
const { isFunctionCall } = await internal_fetchAIChatMessage(messages, assistantId, params);
// 3. place a search with the search working model if this model is not support tool use
const isModelSupportToolUse = aiModelSelectors.isModelSupportToolUse(
model,
provider!,
)(getAiInfraStoreState());
const isAgentEnableSearch = agentChatConfigSelectors.isAgentEnableSearch(getAgentStoreState());
// 4. if it's the function call message, trigger the function method
if (isAgentEnableSearch && !isModelSupportToolUse) {
const { model, provider } = agentChatConfigSelectors.searchFCModel(getAgentStoreState());
let isToolsCalling = false;
let isError = false;
const abortController = get().internal_toggleChatLoading(
true,
assistantId,
n('generateMessage(start)', { messageId: assistantId, messages }) as string,
);
get().internal_toggleSearchWorkflow(true, assistantId);
await chatService.fetchPresetTaskResult({
params: { messages, model, provider, plugins: [WebBrowsingManifest.identifier] },
onFinish: async (_, { toolCalls, usage }) => {
if (toolCalls && toolCalls.length > 0) {
get().internal_toggleToolCallingStreaming(assistantId, undefined);
// update tools calling
await get().internal_updateMessageContent(assistantId, '', {
toolCalls,
metadata: usage,
model,
provider,
});
}
},
abortController,
onMessageHandle: async (chunk) => {
if (chunk.type === 'tool_calls') {
get().internal_toggleSearchWorkflow(false, assistantId);
get().internal_toggleToolCallingStreaming(assistantId, chunk.isAnimationActives);
get().internal_dispatchMessage({
id: assistantId,
type: 'updateMessage',
value: { tools: get().internal_transformToolCalls(chunk.tool_calls) },
});
isToolsCalling = true;
}
if (chunk.type === 'text') {
abortController!.abort('not fc');
}
},
onErrorHandle: async (error) => {
isError = true;
await messageService.updateMessageError(assistantId, error);
await refreshMessages();
},
});
get().internal_toggleChatLoading(
false,
assistantId,
n('generateMessage(start)', { messageId: assistantId, messages }) as string,
);
get().internal_toggleSearchWorkflow(false, assistantId);
// if there is error, then stop
if (isError) return;
// if it's the function call message, trigger the function method
if (isToolsCalling) {
await refreshMessages();
await triggerToolCalls(assistantId, {
threadId: params?.threadId,
inPortalThread: params?.inPortalThread,
});
// then story the workflow
return;
}
}
// 4. fetch the AI response
const { isFunctionCall } = await internal_fetchAIChatMessage({
messages,
messageId: assistantId,
params,
model,
provider: provider!,
});
// 5. if it's the function call message, trigger the function method
if (isFunctionCall) {
await refreshMessages();
await triggerToolCalls(assistantId, {
@@ -348,7 +443,7 @@ export const generateAIChat: StateCreator<
});
}
// 5. summary history if context messages is larger than historyCount
// 6. summary history if context messages is larger than historyCount
const historyCount = agentChatConfigSelectors.historyCount(getAgentStoreState());
if (
@@ -365,7 +460,7 @@ export const generateAIChat: StateCreator<
await get().internal_summaryHistory(historyMessages);
}
},
internal_fetchAIChatMessage: async (messages, messageId, params) => {
internal_fetchAIChatMessage: async ({ messages, messageId, params, provider, model }) => {
const {
internal_toggleChatLoading,
refreshMessages,
@@ -382,7 +477,7 @@ export const generateAIChat: StateCreator<
);
const agentConfig = agentSelectors.currentAgentConfig(getAgentStoreState());
const chatConfig = agentConfig.chatConfig;
const chatConfig = agentChatConfigSelectors.currentChatConfig(getAgentStoreState());
const compiler = template(chatConfig.inputTemplate, { interpolate: /{{([\S\s]+?)}}/g });
@@ -444,8 +539,8 @@ export const generateAIChat: StateCreator<
abortController,
params: {
messages: preprocessMsgs,
model: agentConfig.model,
provider: agentConfig.provider,
model,
provider,
...agentConfig.params,
plugins: agentConfig.plugins,
},
@@ -529,6 +624,7 @@ export const generateAIChat: StateCreator<
});
break;
}
case 'reasoning': {
// if there is no thinkingStartAt, it means the start of reasoning
if (!thinkingStartAt) {
@@ -639,4 +735,8 @@ export const generateAIChat: StateCreator<
`toggleToolCallingStreaming/${!!streaming ? 'start' : 'end'}`,
);
},
internal_toggleSearchWorkflow: (loading, id) => {
return get().internal_toggleLoadingArrays('searchWorkflowLoadingIds', loading, id);
},
});
@@ -15,6 +15,7 @@ export interface ChatAIChatState {
* is the AI message is reasoning
*/
reasoningLoadingIds: string[];
searchWorkflowLoadingIds: string[];
/**
* the tool calling stream ids
*/
@@ -28,5 +29,6 @@ export const initialAiChatState: ChatAIChatState = {
messageRAGLoadingIds: [],
pluginApiLoadingIds: [],
reasoningLoadingIds: [],
searchWorkflowLoadingIds: [],
toolCallingStreamIds: {},
};
+8 -1
View File
@@ -1,9 +1,16 @@
import type { ChatStoreState } from '../../initialState';
const isMessageInReasoning = (id: string) => (s: ChatStoreState) =>
s.reasoningLoadingIds.includes(id);
const isMessageInSearchWorkflow = (id: string) => (s: ChatStoreState) =>
s.searchWorkflowLoadingIds.includes(id);
const isIntentUnderstanding = (id: string) => (s: ChatStoreState) =>
isMessageInSearchWorkflow(id)(s);
export const aiChatSelectors = {
isIntentUnderstanding,
isMessageInReasoning,
isMessageInSearchWorkflow,
};
+9 -1
View File
@@ -81,6 +81,8 @@ export interface ChatMessageAction {
reasoning?: ModelReasoning;
search?: GroundingSearch;
metadata?: MessageMetadata;
model?: string;
provider?: string;
},
) => Promise<void>;
/**
@@ -302,7 +304,11 @@ export const chatMessage: StateCreator<
value: { tools: internal_transformToolCalls(extra?.toolCalls) },
});
} else {
internal_dispatchMessage({ id, type: 'updateMessage', value: { content } });
internal_dispatchMessage({
id,
type: 'updateMessage',
value: { content },
});
}
await messageService.updateMessage(id, {
@@ -311,6 +317,8 @@ export const chatMessage: StateCreator<
reasoning: extra?.reasoning,
search: extra?.search,
metadata: extra?.metadata,
model: extra?.model,
provider: extra?.provider,
});
await refreshMessages();
},
+6 -4
View File
@@ -50,12 +50,13 @@ export interface ChatPluginAction {
traceId?: string;
threadId?: string;
inPortalThread?: boolean;
inSearchWorkflow?: boolean;
}) => Promise<void>;
summaryPluginContent: (id: string) => Promise<void>;
triggerToolCalls: (
id: string,
params?: { threadId?: string; inPortalThread?: boolean },
params?: { threadId?: string; inPortalThread?: boolean; inSearchWorkflow?: boolean },
) => Promise<void>;
updatePluginState: (id: string, value: any) => Promise<void>;
updatePluginArguments: <T = any>(id: string, value: T) => Promise<void>;
@@ -209,7 +210,7 @@ export const chatPlugin: StateCreator<
await get().internal_invokeDifferentTypePlugin(id, payload);
},
triggerAIMessage: async ({ parentId, traceId, threadId, inPortalThread }) => {
triggerAIMessage: async ({ parentId, traceId, threadId, inPortalThread, inSearchWorkflow }) => {
const { internal_coreProcessMessage } = get();
const chats = inPortalThread
@@ -220,6 +221,7 @@ export const chatPlugin: StateCreator<
traceId,
threadId,
inPortalThread,
inSearchWorkflow,
});
},
@@ -245,7 +247,7 @@ export const chatPlugin: StateCreator<
);
},
triggerToolCalls: async (assistantId, { threadId, inPortalThread } = {}) => {
triggerToolCalls: async (assistantId, { threadId, inPortalThread, inSearchWorkflow } = {}) => {
const message = chatSelectors.getMessageById(assistantId)(get());
if (!message || !message.tools) return;
@@ -281,7 +283,7 @@ export const chatPlugin: StateCreator<
const traceId = chatSelectors.getTraceIdByMessageId(latestToolId)(get());
await get().triggerAIMessage({ traceId, threadId, inPortalThread });
await get().triggerAIMessage({ traceId, threadId, inPortalThread, inSearchWorkflow });
},
updatePluginState: async (id, value) => {
const { refreshMessages } = get();
@@ -78,6 +78,10 @@ exports[`settingsSelectors > defaultAgent > should merge DEFAULT_AGENT and s.set
"enableReasoning": false,
"historyCount": 8,
"reasoningBudgetToken": 1024,
"searchFCModel": {
"model": "gpt-4o-mini",
"provider": "openai",
},
"searchMode": "off",
},
"model": "gpt-3.5-turbo",
+6
View File
@@ -62,5 +62,11 @@ export const AgentChatConfigSchema = z.object({
enableReasoningEffort: z.boolean().optional(),
historyCount: z.number().optional(),
reasoningBudgetToken: z.number().optional(),
searchFCModel: z
.object({
model: z.string(),
provider: z.string(),
})
.optional(),
searchMode: z.enum(['off', 'on', 'auto']).optional(),
});
-1
View File
@@ -68,7 +68,6 @@ export interface ChatStreamPayload {
*/
n?: number;
/**
* @deprecated
*
*/
plugins?: string[];