visuddhinanda před 4 měsíci
rodič
revize
772beb2572

+ 18 - 12
dashboard-v4/dashboard/src/components/chat/AssistantMessage.tsx

@@ -9,9 +9,11 @@ import {
 } from "@ant-design/icons";
 import { AssistantMessageProps } from "../../types/chat";
 import Marked from "../general/Marked";
+import { VersionSwitcher } from "./VersionSwitcher";
+import ToolMessage from "./ToolMessage";
 
 const AssistantMessage = ({
-  messages,
+  session,
   onRefresh,
   onEdit,
   isPending,
@@ -19,7 +21,10 @@ const AssistantMessage = ({
   onDislike,
   onCopy,
   onShare,
+  onVersionSwitch,
 }: AssistantMessageProps) => {
+  const messages = session.ai_messages;
+
   const mainMessage = messages.find((m) => m.role === "assistant" && m.content);
   const toolMessages = messages.filter((m) => m.role === "tool");
 
@@ -53,17 +58,7 @@ const AssistantMessage = ({
 
       <div className="message-content" style={{ backgroundColor: "unset" }}>
         {/* Tool calls 显示 */}
-        {toolMessages.length > 0 && (
-          <div className="tool-calls">
-            {toolMessages.map((toolMsg, index) => (
-              <div key={toolMsg.uid} className="tool-result">
-                <span className="tool-label">Tool {index + 1}</span>
-                <div className="tool-content">{toolMsg.content}</div>
-              </div>
-            ))}
-          </div>
-        )}
-
+        <ToolMessage session={session} />
         {/* 主要回答内容 */}
         {mainMessage?.content && (
           <div className="message-text">
@@ -111,6 +106,17 @@ const AssistantMessage = ({
               icon={<ShareAltOutlined />}
               onClick={handleShare}
             />
+            {/* 版本切换器 */}
+            {session.versions && session.versions.length > 1 && (
+              <VersionSwitcher
+                versions={session.versions}
+                currentVersion={session.current_version}
+                onSwitch={(versionIndex) =>
+                  onVersionSwitch &&
+                  onVersionSwitch(session.versions[versionIndex].message_id)
+                }
+              />
+            )}
             <Button
               size="small"
               type="text"

+ 289 - 41
dashboard-v4/dashboard/src/components/chat/ChatInput.tsx

@@ -1,26 +1,49 @@
-import React, { useState, useCallback, useEffect } from "react";
+import React, { useState, useCallback, useEffect, useRef } from "react";
 import {
   Affix,
+  AutoComplete,
   Button,
   Card,
   Dropdown,
   Input,
   MenuProps,
+  Select,
   Space,
   Tooltip,
+  Typography,
+  Tag,
 } from "antd";
 import {
   SendOutlined,
   PaperClipOutlined,
   DownOutlined,
+  SearchOutlined,
 } from "@ant-design/icons";
 import { ChatInputProps } from "../../types/chat";
 import PromptButtonGroup from "./PromptButtonGroup";
 import { IAiModel } from "../api/ai";
 import { useAppSelector } from "../../hooks";
 import { siteInfo } from "../../reducers/layout";
+import { backend, get } from "../../request";
+import { SuggestionsResponse } from "../../types/search";
 
 const { TextArea } = Input;
+const { Text } = Typography;
+
+// 定义建议项类型
+interface SuggestionOption {
+  value: string;
+  label: React.ReactNode;
+  text: string;
+  source: string;
+  score: number;
+  resource_type?: string;
+  language?: string;
+  doc_id?: string;
+}
+
+// 定义搜索模式类型
+type SearchMode = "auto" | "none" | "team" | "word" | "explain" | "title";
 
 export function ChatInput({
   onSend,
@@ -31,8 +54,15 @@ export function ChatInput({
   const [inputValue, setInputValue] = useState("");
   const [selectedModel, setSelectedModel] = useState<string>("");
   const [models, setModels] = useState<IAiModel[]>();
+  const [searchMode, setSearchMode] = useState<SearchMode>("auto");
+  const [suggestions, setSuggestions] = useState<SuggestionOption[]>([]);
+  const [loading, setLoading] = useState(false);
   const site = useAppSelector(siteInfo);
 
+  // 使用 ref 来防止过于频繁的请求
+  const abortControllerRef = useRef<AbortController | null>(null);
+  const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
+
   useEffect(() => {
     const allModels = site?.settings?.models?.chat ?? [];
     setModels(allModels);
@@ -46,11 +76,177 @@ export function ChatInput({
     }
   }, [onModelChange, site?.settings?.models?.chat]);
 
+  // 获取搜索建议
+  const fetchSuggestions = useCallback(
+    async (query: string) => {
+      // 取消之前的请求
+      if (abortControllerRef.current) {
+        abortControllerRef.current.abort();
+      }
+
+      // 如果查询为空或搜索模式为 none,清空建议
+      if (!query.trim() || searchMode === "none") {
+        setSuggestions([]);
+        return;
+      }
+
+      // 创建新的 AbortController
+      abortControllerRef.current = new AbortController();
+
+      setLoading(true);
+
+      try {
+        // 根据搜索模式确定查询字段
+        let fields: string | undefined;
+        switch (searchMode) {
+          case "title":
+            fields = "title";
+            break;
+          case "team":
+          case "word":
+          case "explain":
+            fields = "title,content";
+            break;
+          case "auto":
+          default:
+            // 不指定 fields,查询所有字段
+            fields = undefined;
+        }
+
+        // 构建查询参数
+        const params = new URLSearchParams({
+          q: query,
+          limit: "10",
+        });
+
+        if (fields) {
+          params.append("fields", fields);
+        }
+
+        // 发起请求
+        const url = `/v3/search-suggest?${params.toString()}`;
+        // 发起请求
+        const response = await fetch(backend(url), {
+          signal: abortControllerRef.current.signal,
+        });
+
+        if (!response.ok) {
+          throw new Error("搜索建议请求失败");
+        }
+
+        const data: SuggestionsResponse = await response.json();
+
+        if (data.success && data.data.suggestions) {
+          // 转换为 AutoComplete 选项格式
+          const options: SuggestionOption[] = data.data.suggestions.map(
+            (item: any) => ({
+              value: item.text,
+              label: renderSuggestionItem(item),
+              text: item.text,
+              source: item.source,
+              score: item.score,
+              resource_type: item.resource_type,
+              language: item.language,
+              doc_id: item.doc_id,
+            })
+          );
+
+          setSuggestions(options);
+        } else {
+          setSuggestions([]);
+        }
+      } catch (error: any) {
+        // 忽略取消的请求
+        if (error.name === "AbortError") {
+          return;
+        }
+        console.error("获取搜索建议失败:", error);
+        setSuggestions([]);
+      } finally {
+        setLoading(false);
+      }
+    },
+    [searchMode]
+  );
+
+  // 渲染建议项
+  const renderSuggestionItem = (item: any) => {
+    // 来源标签颜色映射
+    const sourceColors: Record<string, string> = {
+      title: "blue",
+      content: "green",
+      page_refs: "orange",
+    };
+
+    // 语言标签
+    const languageLabels: Record<string, string> = {
+      pali: "巴利文",
+      zh: "中文",
+      en: "英文",
+    };
+
+    return (
+      <div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
+        <span style={{ flex: 1 }}>{item.text}</span>
+        <Space size={4}>
+          {item.source && (
+            <Tag
+              color={sourceColors[item.source] || "default"}
+              style={{ margin: 0, fontSize: "12px" }}
+            >
+              {item.source}
+            </Tag>
+          )}
+          {item.language && (
+            <Tag style={{ margin: 0, fontSize: "12px" }}>
+              {languageLabels[item.language] || item.language}
+            </Tag>
+          )}
+        </Space>
+      </div>
+    );
+  };
+
+  // 处理输入变化(带防抖)
+  const handleInputChange = useCallback(
+    (value: string) => {
+      setInputValue(value);
+
+      // 清除之前的定时器
+      if (debounceTimerRef.current) {
+        clearTimeout(debounceTimerRef.current);
+      }
+
+      // 如果输入为空,直接清空建议
+      if (!value.trim()) {
+        setSuggestions([]);
+        return;
+      }
+
+      // 设置新的防抖定时器(300ms)
+      debounceTimerRef.current = setTimeout(() => {
+        fetchSuggestions(value);
+      }, 300);
+    },
+    [fetchSuggestions]
+  );
+
+  // 处理选择建议项
+  const handleSelect = useCallback(
+    (value: string, option: SuggestionOption) => {
+      setInputValue(value);
+      // 选择后清空建议列表
+      setSuggestions([]);
+    },
+    []
+  );
+
   const handleSend = useCallback(() => {
     if (!inputValue.trim() || disabled) return;
 
     onSend(inputValue.trim());
     setInputValue("");
+    setSuggestions([]);
   }, [inputValue, disabled, onSend]);
 
   const handleKeyPress = useCallback(
@@ -75,50 +271,98 @@ export function ChatInput({
       label: model.name,
     })),
   };
-  /**
- *     <div className="chat-input">
-      <div className="input-area">
-        <TextArea
-          value={inputValue}
-          onChange={(e) => setInputValue(e.target.value)}
-          onKeyPress={handleKeyPress}
-          placeholder={placeholder || "输入你的问题..."}
-          autoSize={{ minRows: 1, maxRows: 6 }}
-          disabled={disabled}
-        />
-
-        <div className="input-actions">
-          <Space>
-            <Button
-              size="small"
-              type="text"
-              icon={<PaperClipOutlined />}
-              disabled={disabled}
-            />
-            <Button
-              type="primary"
-              icon={<SendOutlined />}
-              onClick={handleSend}
-              disabled={!inputValue.trim() || disabled}
-            />
-          </Space>
-        </div>
-      </div>
-    </div>
- */
+
+  const handleSearchModeChange = (value: SearchMode) => {
+    setSearchMode(value);
+    // 模式改变时,如果有输入内容,重新获取建议
+    if (inputValue.trim() && value !== "none") {
+      fetchSuggestions(inputValue);
+    } else {
+      setSuggestions([]);
+    }
+  };
+
+  // 组件卸载时清理
+  useEffect(() => {
+    return () => {
+      if (abortControllerRef.current) {
+        abortControllerRef.current.abort();
+      }
+      if (debounceTimerRef.current) {
+        clearTimeout(debounceTimerRef.current);
+      }
+    };
+  }, []);
+
   return (
     <Affix offsetBottom={10}>
       <Card style={{ borderRadius: "10px", borderColor: "#d9d9d9" }}>
         <div style={{ maxWidth: "1200px", margin: "0 auto" }}>
-          <div style={{ display: "flex", marginBottom: "8px" }}>
-            <TextArea
+          <div style={{ display: "flex", marginBottom: "8px", gap: "8px" }}>
+            <Space>
+              <SearchOutlined />
+              <Select
+                placement="topLeft"
+                value={searchMode}
+                style={{ width: 120 }}
+                onChange={handleSearchModeChange}
+                options={[
+                  {
+                    value: "auto",
+                    label: (
+                      <div>
+                        <div>{"自动"}</div>
+                        <div>
+                          <Text type="secondary" style={{ fontSize: "85%" }}>
+                            关键词+语义模糊搜索
+                          </Text>
+                        </div>
+                      </div>
+                    ),
+                  },
+                  {
+                    value: "none",
+                    label: "关闭",
+                  },
+                  {
+                    value: "team",
+                    label: "术语百科",
+                  },
+                  {
+                    value: "word",
+                    label: "词义辨析",
+                  },
+                  {
+                    value: "explain",
+                    label: "经文解析",
+                  },
+                  {
+                    value: "title",
+                    label: "标题搜索",
+                  },
+                ]}
+              />
+            </Space>
+
+            <AutoComplete
+              style={{ flex: 1 }}
+              placement="topLeft"
               value={inputValue}
-              onChange={(e) => setInputValue(e.target.value)}
-              onKeyPress={handleKeyPress}
-              placeholder={placeholder || "提出你的问题,如:总结下面的内容..."}
-              autoSize={{ minRows: 1, maxRows: 6 }}
-              style={{ resize: "none", paddingRight: "48px" }}
-            />
+              options={suggestions}
+              onSelect={handleSelect}
+              onChange={handleInputChange}
+              notFoundContent={loading ? "搜索中..." : null}
+              disabled={disabled}
+            >
+              <TextArea
+                onKeyPress={handleKeyPress}
+                placeholder={
+                  placeholder || "提出你的问题,如:总结下面的内容..."
+                }
+                autoSize={{ minRows: 1, maxRows: 6 }}
+                style={{ resize: "none", paddingRight: "48px" }}
+              />
+            </AutoComplete>
           </div>
 
           <div
@@ -135,7 +379,11 @@ export function ChatInput({
               <PromptButtonGroup onText={setInputValue} />
             </Space>
             <Space>
-              <Dropdown menu={modelMenu} trigger={["click"]}>
+              <Dropdown
+                placement="topLeft"
+                menu={modelMenu}
+                trigger={["click"]}
+              >
                 <Button size="small" type="text">
                   {models?.find((m) => m.uid === selectedModel)?.name}
                   <DownOutlined />

+ 2 - 12
dashboard-v4/dashboard/src/components/chat/SessionGroup.tsx

@@ -53,21 +53,10 @@ export function SessionGroup({
           </div>
         )}
 
-        {/* 版本切换器 */}
-        {hasMultipleVersions && !isPending && !hasFailed && (
-          <VersionSwitcher
-            versions={session.versions}
-            currentVersion={session.current_version}
-            onSwitch={(versionIndex) =>
-              onVersionSwitch(session.versions[versionIndex].message_id)
-            }
-          />
-        )}
-
         {/* AI消息内容 */}
         {!hasFailed && session.ai_messages.length > 0 && (
           <AssistantMessage
-            messages={session.ai_messages}
+            session={session}
             onRefresh={() => onRefresh && onRefresh(session.session_id)}
             onEdit={(content) => onEdit && onEdit(session.session_id, content)}
             isPending={isPending}
@@ -75,6 +64,7 @@ export function SessionGroup({
             onDislike={onDislike}
             onCopy={onCopy}
             onShare={onShare}
+            onVersionSwitch={onVersionSwitch}
           />
         )}
       </div>

+ 2 - 1
dashboard-v4/dashboard/src/components/chat/StreamingMessage.tsx

@@ -1,4 +1,5 @@
 import React from "react";
+import Marked from "../general/Marked";
 
 interface StreamingMessageProps {
   content: string;
@@ -15,7 +16,7 @@ export function StreamingMessage({ content }: StreamingMessageProps) {
 
       <div className="message-content">
         <div className="message-text">
-          {content}
+          <Marked text={content} />
           <span className="cursor">|</span>
         </div>
       </div>

+ 10 - 0
dashboard-v4/dashboard/src/components/chat/style.css

@@ -1,6 +1,7 @@
 .chat-container {
   display: flex;
   flex-direction: column;
+  min-height: 100vh;
 }
 
 .messages-area {
@@ -28,6 +29,15 @@
   margin-bottom: 12px;
 }
 
+.user-message {
+  display: flex;
+  justify-content: flex-end;
+}
+.user-message .message-content {
+  border: 1px solid rgba(128, 128, 128, 0.5);
+  background-color: rgba(255, 255, 255, 0.5);
+}
+
 .message-header {
   display: flex;
   justify-content: space-between;

+ 7 - 1
dashboard-v4/dashboard/src/pages/library/chat/ai.tsx

@@ -4,7 +4,13 @@ import { ChatContainer } from "../../../components/chat/ChatContainer";
 const AI = () => {
   const { id } = useParams();
 
-  return <ChatContainer chatId={id ?? ""} />;
+  return (
+    <div>
+      <div style={{ width: 1000, marginLeft: "auto", marginRight: "auto" }}>
+        <ChatContainer chatId={id ?? ""} />
+      </div>
+    </div>
+  );
 };
 
 export default AI;

+ 13 - 1
dashboard-v4/dashboard/src/pages/library/chat/new.tsx

@@ -1,7 +1,19 @@
 import { ChatContainer } from "../../../components/chat/ChatContainer";
 
 const AI = () => {
-  return <ChatContainer chatId="" />;
+  return (
+    <div>
+      <div
+        style={{
+          width: 1000,
+          marginLeft: "auto",
+          marginRight: "auto",
+        }}
+      >
+        <ChatContainer chatId={""} />
+      </div>
+    </div>
+  );
 };
 
 export default AI;

+ 131 - 14
dashboard-v4/dashboard/src/services/agentApi.ts

@@ -1,11 +1,11 @@
+import { FunctionDefinition } from "../types/chat";
 import {
   SearchByQueryArgs,
   SearchByPageRefArgs,
   GetTermDefinitionArgs,
   SearchPaliArgs,
   SearchResponse,
-  AICallbackFunction,
-} from "../types/agent"; // 假设你的类型定义文件名为 apiTypes.ts
+} from "../types/search"; // 假设你的类型定义文件名为 apiTypes.ts
 
 /**
  * 基础 API URL
@@ -13,6 +13,40 @@ import {
  */
 const API_BASE_URL = "http://localhost:8000/api/v3";
 
+export const system_prompt = `
+你是一个专业的、精通巴利语和汉语的佛教文献检索助手。你的任务是分析用户的查询,并利用你拥有的工具(函数)来获取信息。
+
+严格遵守以下原则:
+1.  优先并恰当地使用提供的工具来满足用户的查询需求。
+2.  你的回答必须简洁、直接,仅包含从工具中获得的信息,不添加任何额外闲聊。
+3.  如果一个查询无法被任何工具处理,或者需要与用户进行澄清,请清晰地说明。
+4.  当用户的查询包含**页码标记**(例如:M3.58, V3.81)时,必须使用 search_by_page_ref 函数。
+5.  当用户的查询是**明确的佛教术语或词汇**(例如:四圣谛, mettā)时,必须使用 search_pali 和 get_term_definition 函数 进行两次搜索,获取巴利文原文和术语字典结果。
+6.  当用户的查询是**描述性、自然语言问题**(例如:佛陀关于慈悲的教导)时,必须使用 search_by_query 函数。
+7.  当用户的查询是**普通关键词**(例如:比丘, 袈裟)时,使用 search_by_query 函数。
+
+以下是你的工具箱:
+- search_by_query: 用于通用的模糊和语义搜索。
+- search_by_page_ref: 专门用于处理页码搜索。
+- get_term_definition: 专门用于获取术语定义。
+- search_pali: 专门用于处理巴利文搜索。
+
+请严格根据上述原则,选择最恰当的工具来处理用户的请求。
+如果用户追问的问题可以使用之前的数据回答,那么无需再次调用工具。
+`;
+
+export type AICallbackFunction = {
+  name:
+    | "search_by_query"
+    | "search_by_page_ref"
+    | "get_term_definition"
+    | "search_pali";
+  arguments:
+    | SearchByQueryArgs
+    | SearchByPageRefArgs
+    | GetTermDefinitionArgs
+    | SearchPaliArgs;
+};
 // ---------------------------------------------------------------- //
 //                  低层 API 客户端(使用 fetch)                  //
 // ---------------------------------------------------------------- //
@@ -59,21 +93,18 @@ const apiClient = async <T>(
 /**
  * 通用搜索函数,处理模糊和语义查询。
  */
-const searchByQuery = async (
+export const searchByQuery = async (
   args: SearchByQueryArgs
 ): Promise<SearchResponse> => {
   return apiClient<SearchResponse>("/search", {
     q: args.query,
-    search_mode: args.search_mode,
-    resource_type: args.resource_type,
-    language: args.language,
   });
 };
 
 /**
  * 专门处理页码搜索的函数。
  */
-const searchByPageRef = async (
+export const searchByPageRef = async (
   args: SearchByPageRefArgs
 ): Promise<SearchResponse> => {
   return apiClient<SearchResponse>("/search", {
@@ -86,24 +117,25 @@ const searchByPageRef = async (
 /**
  * 专门用于获取术语定义的函数。
  */
-const getTermDefinition = async (
+export const getTermDefinition = async (
   args: GetTermDefinitionArgs
 ): Promise<SearchResponse> => {
   return apiClient<SearchResponse>("/search", {
     q: args.term,
-    search_mode: "exact", // 固定为精确搜索
-    resource_type: ["dictionary"], // 仅搜索字典类型
+    resource_type: "term",
   });
 };
 
 /**
  * 专门用于巴利文精确搜索的函数。
  */
-const searchPali = async (args: SearchPaliArgs): Promise<SearchResponse> => {
+export const searchPali = async (
+  args: SearchPaliArgs
+): Promise<SearchResponse> => {
   return apiClient<SearchResponse>("/search", {
     q: args.query,
-    search_mode: "exact", // 巴利文搜索通常是精确的
-    language: ["pali"], // 仅搜索巴利文
+    resource_type: "original_text", // 仅搜索原文
+    language: "pali", // 仅搜索巴利文
   });
 };
 
@@ -138,6 +170,92 @@ export const handleFunctionCall = async (
   }
 };
 
+export const tools: FunctionDefinition[] = [
+  {
+    type: "function",
+    function: {
+      name: "search_by_query",
+      description: "通用搜索函数,支持模糊与语义查询。",
+      parameters: {
+        type: "object",
+        properties: {
+          query: {
+            type: "string",
+            description: "用户输入的查询语句(如: 佛陀关于慈悲的教导)",
+          },
+          search_mode: {
+            type: "string",
+            enum: ["fuzzy", "semantic"],
+            description: "搜索模式: fuzzy=模糊, semantic=语义",
+          },
+        },
+        required: ["query", "search_mode"],
+        additionalProperties: false,
+      },
+    },
+    strict: true,
+  },
+  {
+    type: "function",
+    function: {
+      name: "search_by_page_ref",
+      description: "根据页码标记(如 M3.58, V3.81)搜索对应内容。",
+      parameters: {
+        type: "object",
+        properties: {
+          page_refs: {
+            type: "string",
+            description: "经文页码标记(如 M3.58)",
+          },
+        },
+        required: ["page_refs"],
+        additionalProperties: false,
+      },
+    },
+    strict: true,
+  },
+  {
+    type: "function",
+    function: {
+      name: "get_term_definition",
+      description: "获取佛教术语的精确定义,主要从字典资源。",
+      parameters: {
+        type: "object",
+        properties: {
+          term: {
+            type: "string",
+            description: "佛教术语(如: 四圣谛, 五蕴, 涅槃)",
+          },
+        },
+        required: ["term"],
+        additionalProperties: false,
+      },
+    },
+    strict: true,
+  },
+  {
+    type: "function",
+    function: {
+      name: "search_pali",
+      description:
+        "在巴利文语料中进行关键词搜索,被搜索词可以是巴利文或者其他语言。",
+      parameters: {
+        type: "object",
+        properties: {
+          query: {
+            type: "string",
+            description:
+              "被搜索词,巴利文词汇或短语(如: mettā, anicca)或者其他语言的佛教专有名词",
+          },
+        },
+        required: ["query"],
+        additionalProperties: false,
+      },
+    },
+    strict: true,
+  },
+];
+
 // ---------------------------------------------------------------- //
 //                  使用示例                                       //
 // ---------------------------------------------------------------- //
@@ -169,4 +287,3 @@ const main = async () => {
   }
 };
  */
-// main();

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 16 - 0
dashboard-v4/dashboard/src/services/mockMessageApi.ts


+ 64 - 5
dashboard-v4/dashboard/src/services/modelAdapters/base.ts

@@ -1,3 +1,6 @@
+// dashboard-v4/dashboard/src/services/modelAdapters/base.ts
+
+import { IAiModel } from "../../components/api/ai";
 import {
   ModelAdapter,
   OpenAIMessage,
@@ -5,17 +8,36 @@ import {
   ParsedChunk,
   ToolCall,
 } from "../../types/chat";
+import {
+  getArgs,
+  GetTermDefinitionArgs,
+  SearchByPageRefArgs,
+  SearchByQueryArgs,
+  SearchPaliArgs,
+  SearchResponse,
+} from "../../types/search";
+import {
+  getTermDefinition,
+  searchByPageRef,
+  searchByQuery,
+  searchPali,
+  tools,
+} from "../agentApi";
 
 export abstract class BaseModelAdapter implements ModelAdapter {
   abstract name: string;
   abstract supportsFunctionCall: boolean;
+  abstract model: IAiModel | undefined;
 
   abstract sendMessage(
     messages: OpenAIMessage[],
     options: SendOptions
   ): Promise<AsyncIterable<string>>;
   abstract parseStreamChunk(chunk: string): ParsedChunk | null;
-  abstract handleFunctionCall(functionCall: ToolCall): Promise<any>;
+
+  public setModel(model: IAiModel) {
+    this.model = model;
+  }
 
   protected createStreamController() {
     return {
@@ -31,16 +53,53 @@ export abstract class BaseModelAdapter implements ModelAdapter {
   protected buildRequestPayload(
     messages: OpenAIMessage[],
     options: SendOptions
-  ) {
+  ): SendOptions {
     return {
-      model: this.name,
+      model: this.model?.name,
       messages,
       stream: true,
       temperature: options.temperature || 0.7,
       max_tokens: options.max_tokens || 2048,
       top_p: options.top_p || 1,
-      functions: options.functions,
-      function_call: options.function_call || "auto",
+      tools: tools,
+      tool_choice: "auto",
     };
   }
+  // ---------------------------------------------------------------- //
+  //               核心 Function Calling 处理函数                     //
+  // ---------------------------------------------------------------- //
+
+  /**
+   * 核心函数:根据 AI 助手返回的函数调用对象,执行相应的操作。
+   *
+   * @param functionCall AI 助手返回的函数调用对象。
+   * @returns 返回一个 Promise,包含搜索结果。
+   */
+  async handleFunctionCall(functionCall: ToolCall): Promise<SearchResponse> {
+    console.info("ai chat function call", functionCall);
+    switch (functionCall.function.name) {
+      case "search_by_query":
+        return searchByQuery(
+          getArgs<SearchByQueryArgs>(functionCall.function.arguments)
+        );
+
+      case "search_by_page_ref":
+        return searchByPageRef(
+          getArgs<SearchByPageRefArgs>(functionCall.function.arguments)
+        );
+
+      case "get_term_definition":
+        return getTermDefinition(
+          getArgs<GetTermDefinitionArgs>(functionCall.function.arguments)
+        );
+
+      case "search_pali":
+        return searchPali(
+          getArgs<SearchPaliArgs>(functionCall.function.arguments)
+        );
+
+      default:
+        throw new Error(`Unknown function call: ${functionCall.function.name}`);
+    }
+  }
 }

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů