diff --git a/client/agent_manage.go b/client/agent_manage.go index 190aca6cf..3272898f6 100644 --- a/client/agent_manage.go +++ b/client/agent_manage.go @@ -71,59 +71,85 @@ func AllKBSelectionModes() []KBSelectionMode { // AgentConfig represents the configuration for an agent. // Field names and JSON tags mirror internal/types.CustomAgentConfig. type AgentConfig struct { - AgentMode string `json:"agent_mode"` - AgentType string `json:"agent_type,omitempty"` - SystemPrompt string `json:"system_prompt"` - SystemPromptID string `json:"system_prompt_id,omitempty"` - ContextTemplate string `json:"context_template"` - ContextTemplateID string `json:"context_template_id,omitempty"` - ModelID string `json:"model_id"` - RerankModelID string `json:"rerank_model_id"` - Temperature float64 `json:"temperature"` - MaxCompletionTokens int `json:"max_completion_tokens"` - Thinking *bool `json:"thinking"` - MaxIterations int `json:"max_iterations"` - LLMCallTimeout int `json:"llm_call_timeout,omitempty"` - AllowedTools []string `json:"allowed_tools"` - MCPSelectionMode string `json:"mcp_selection_mode"` - MCPServices []string `json:"mcp_services"` - SkillsSelectionMode string `json:"skills_selection_mode"` - SelectedSkills []string `json:"selected_skills"` - KBSelectionMode string `json:"kb_selection_mode"` - KnowledgeBases []string `json:"knowledge_bases"` - RetrieveKBOnlyWhenMentioned bool `json:"retrieve_kb_only_when_mentioned"` - RetainRetrievalHistory bool `json:"retain_retrieval_history"` - ImageUploadEnabled bool `json:"image_upload_enabled"` - VLMModelID string `json:"vlm_model_id"` - AudioUploadEnabled bool `json:"audio_upload_enabled"` - ASRModelID string `json:"asr_model_id"` - ImageStorageProvider string `json:"image_storage_provider"` - SupportedFileTypes []string `json:"supported_file_types"` - DataAnalysisEnabled bool `json:"data_analysis_enabled"` - FAQPriorityEnabled bool `json:"faq_priority_enabled"` - FAQDirectAnswerThreshold float64 `json:"faq_direct_answer_threshold"` - FAQScoreBoost float64 `json:"faq_score_boost"` - WebSearchEnabled bool `json:"web_search_enabled"` - WebSearchMaxResults int `json:"web_search_max_results"` - WebSearchProviderID string `json:"web_search_provider_id,omitempty"` - WebFetchEnabled bool `json:"web_fetch_enabled"` - WebFetchTopN int `json:"web_fetch_top_n,omitempty"` - MultiTurnEnabled bool `json:"multi_turn_enabled"` - HistoryTurns int `json:"history_turns"` - EmbeddingTopK int `json:"embedding_top_k"` - KeywordThreshold float64 `json:"keyword_threshold"` - VectorThreshold float64 `json:"vector_threshold"` - RerankTopK int `json:"rerank_top_k"` - RerankThreshold float64 `json:"rerank_threshold"` - EnableQueryExpansion bool `json:"enable_query_expansion"` - EnableRewrite bool `json:"enable_rewrite"` - RewritePromptSystem string `json:"rewrite_prompt_system"` - RewritePromptUser string `json:"rewrite_prompt_user"` - QueryUnderstandModelID string `json:"query_understand_model_id,omitempty"` - FallbackStrategy string `json:"fallback_strategy"` - FallbackResponse string `json:"fallback_response"` - FallbackPrompt string `json:"fallback_prompt"` - SuggestedPrompts []string `json:"suggested_prompts,omitempty"` + AgentMode string `json:"agent_mode"` + AgentType string `json:"agent_type,omitempty"` + SystemPrompt string `json:"system_prompt"` + SystemPromptID string `json:"system_prompt_id,omitempty"` + ContextTemplate string `json:"context_template"` + ContextTemplateID string `json:"context_template_id,omitempty"` + ModelID string `json:"model_id"` + RerankModelID string `json:"rerank_model_id"` + Temperature float64 `json:"temperature"` + MaxCompletionTokens int `json:"max_completion_tokens"` + Thinking *bool `json:"thinking"` + MaxIterations int `json:"max_iterations"` + LLMCallTimeout int `json:"llm_call_timeout,omitempty"` + AllowedTools []string `json:"allowed_tools"` + MCPSelectionMode string `json:"mcp_selection_mode"` + MCPServices []string `json:"mcp_services"` + SkillsSelectionMode string `json:"skills_selection_mode"` + SelectedSkills []string `json:"selected_skills"` + KBSelectionMode string `json:"kb_selection_mode"` + KnowledgeBases []string `json:"knowledge_bases"` + RetrieveKBOnlyWhenMentioned bool `json:"retrieve_kb_only_when_mentioned"` + RetainRetrievalHistory bool `json:"retain_retrieval_history"` + ImageUploadEnabled bool `json:"image_upload_enabled"` + VLMModelID string `json:"vlm_model_id"` + AudioUploadEnabled bool `json:"audio_upload_enabled"` + ASRModelID string `json:"asr_model_id"` + ImageStorageProvider string `json:"image_storage_provider"` + SupportedFileTypes []string `json:"supported_file_types"` + DataAnalysisEnabled bool `json:"data_analysis_enabled"` + FAQPriorityEnabled bool `json:"faq_priority_enabled"` + FAQDirectAnswerThreshold float64 `json:"faq_direct_answer_threshold"` + FAQScoreBoost float64 `json:"faq_score_boost"` + WebSearchEnabled bool `json:"web_search_enabled"` + WebSearchMaxResults int `json:"web_search_max_results"` + WebSearchProviderID string `json:"web_search_provider_id,omitempty"` + WebFetchEnabled bool `json:"web_fetch_enabled"` + WebFetchTopN int `json:"web_fetch_top_n,omitempty"` + MultiTurnEnabled bool `json:"multi_turn_enabled"` + HistoryTurns int `json:"history_turns"` + EmbeddingTopK int `json:"embedding_top_k"` + KeywordThreshold float64 `json:"keyword_threshold"` + VectorThreshold float64 `json:"vector_threshold"` + RerankTopK int `json:"rerank_top_k"` + RerankThreshold float64 `json:"rerank_threshold"` + EnableQueryExpansion bool `json:"enable_query_expansion"` + EnableRewrite bool `json:"enable_rewrite"` + RewritePromptSystem string `json:"rewrite_prompt_system"` + RewritePromptUser string `json:"rewrite_prompt_user"` + QueryUnderstandModelID string `json:"query_understand_model_id,omitempty"` + FallbackStrategy string `json:"fallback_strategy"` + FallbackResponse string `json:"fallback_response"` + FallbackPrompt string `json:"fallback_prompt"` + QuestionSuggestions *QuestionSuggestionConfig `json:"question_suggestions,omitempty"` +} + +type QuestionSuggestionConfig struct { + Starters StarterSuggestionConfig `json:"starters"` + FollowUps FollowUpSuggestionConfig `json:"follow_ups"` +} + +type StarterSuggestionConfig struct { + Enabled bool `json:"enabled"` + Mode string `json:"mode"` + Items []string `json:"items"` + Count int `json:"count"` +} + +type FollowUpSuggestionConfig struct { + Enabled bool `json:"enabled"` + Mode string `json:"mode"` + Count int `json:"count"` + ModelID string `json:"model_id,omitempty"` + AdditionalInstruction string `json:"additional_instruction,omitempty"` + Categories []string `json:"categories,omitempty"` + MaxContextTurns int `json:"max_context_turns"` + SuppressOnFallback bool `json:"suppress_on_fallback"` + SuppressWhenAnswerAsksQuestion bool `json:"suppress_when_answer_asks_question"` + KnowledgeFallback bool `json:"knowledge_fallback"` + AllowRegenerate bool `json:"allow_regenerate"` } // CreateAgentRequest represents the request to create an agent. diff --git a/client/message_suggestion.go b/client/message_suggestion.go new file mode 100644 index 000000000..9b6b070a8 --- /dev/null +++ b/client/message_suggestion.go @@ -0,0 +1,85 @@ +package client + +import ( + "context" + "fmt" + "net/http" + "net/url" +) + +type MessageSuggestionItem struct { + ID string `json:"id"` + Text string `json:"text"` + Category string `json:"category,omitempty"` + Source string `json:"source"` + KnowledgeBaseIDs []string `json:"knowledge_base_ids,omitempty"` +} + +type MessageSuggestionSet struct { + ID string `json:"id"` + SessionID string `json:"session_id"` + AssistantMessageID string `json:"assistant_message_id"` + Status string `json:"status"` + AllowRegenerate bool `json:"allow_regenerate"` + SuppressionReason string `json:"suppression_reason,omitempty"` + Questions []MessageSuggestionItem `json:"questions"` +} + +type messageSuggestionResponse struct { + Success bool `json:"success"` + Data MessageSuggestionSet `json:"data"` +} + +func (c *Client) EnsureMessageSuggestions( + ctx context.Context, + sessionID string, + messageID string, + regenerate bool, +) (*MessageSuggestionSet, error) { + path := fmt.Sprintf("/api/v1/sessions/%s/messages/%s/suggestions", url.PathEscape(sessionID), url.PathEscape(messageID)) + resp, err := c.doRequest(ctx, http.MethodPost, path, map[string]bool{"regenerate": regenerate}, nil) + if err != nil { + return nil, err + } + var response messageSuggestionResponse + if err := parseResponse(resp, &response); err != nil { + return nil, err + } + return &response.Data, nil +} + +func (c *Client) GetMessageSuggestions( + ctx context.Context, + sessionID string, + messageID string, +) (*MessageSuggestionSet, error) { + path := fmt.Sprintf("/api/v1/sessions/%s/messages/%s/suggestions", url.PathEscape(sessionID), url.PathEscape(messageID)) + resp, err := c.doRequest(ctx, http.MethodGet, path, nil, nil) + if err != nil { + return nil, err + } + var response messageSuggestionResponse + if err := parseResponse(resp, &response); err != nil { + return nil, err + } + return &response.Data, nil +} + +func (c *Client) RecordMessageSuggestionEvent( + ctx context.Context, + sessionID string, + setID string, + questionID string, + eventType string, +) error { + path := fmt.Sprintf("/api/v1/sessions/%s/suggestion-events", url.PathEscape(sessionID)) + resp, err := c.doRequest(ctx, http.MethodPost, path, map[string]string{ + "suggestion_set_id": setID, + "question_id": questionID, + "event_type": eventType, + }, nil) + if err != nil { + return err + } + return parseResponse(resp, nil) +} diff --git a/docs/api/agent.md b/docs/api/agent.md index 7f6f4e5be..c0626e397 100644 --- a/docs/api/agent.md +++ b/docs/api/agent.md @@ -493,9 +493,27 @@ curl --location 'http://localhost:8080/api/v1/agents/placeholders' \ ### 推荐问题设置 +`question_suggestions` 是智能体拥有的统一策略。网页嵌入等渠道只能关闭展示,不能覆盖内容或生成规则。 + | 参数 | 类型 | 默认值 | 说明 | |------|------|--------|------| -| `suggested_prompts` | []string | - | 推荐问题列表,用于在前端对话面板展示快捷提问 | +| `question_suggestions.starters.enabled` | bool | true | 是否在首次提问前展示开场问题 | +| `question_suggestions.starters.mode` | string | `hybrid` | `curated`、`knowledge` 或 `hybrid` | +| `question_suggestions.starters.items` | []string | `[]` | 运营配置的开场问题 | +| `question_suggestions.starters.count` | int | 6 | 展示数量,范围 1-8 | +| `question_suggestions.follow_ups.enabled` | bool | false | 是否在每次完整回答后异步生成追问 | +| `question_suggestions.follow_ups.mode` | string | `hybrid` | `generated`、`knowledge` 或 `hybrid` | +| `question_suggestions.follow_ups.count` | int | 3 | 生成数量,范围 1-5 | +| `question_suggestions.follow_ups.model_id` | string | - | 独立生成模型;为空使用本轮对话模型 | +| `question_suggestions.follow_ups.categories` | []string | `clarify,deepen,action` | 允许的问题类型 | +| `question_suggestions.follow_ups.max_context_turns` | int | 2 | 生成时使用的最近对话轮数,范围 1-5 | +| `question_suggestions.follow_ups.additional_instruction` | string | - | 智能体作者的附加生成要求 | +| `question_suggestions.follow_ups.suppress_on_fallback` | bool | true | 兜底回答后不展示 | +| `question_suggestions.follow_ups.suppress_when_answer_asks_question` | bool | true | 回答本身以问题结尾时不展示 | +| `question_suggestions.follow_ups.knowledge_fallback` | bool | true | 模型失败时使用知识库候选补位 | +| `question_suggestions.follow_ups.allow_regenerate` | bool | false | 是否允许用户换一批 | + +旧 `suggested_prompts` 会在数据库迁移时一次性写入 `starters.items`,API 不再接受该字段。 ### 高级设置 diff --git a/docs/api/chat.md b/docs/api/chat.md index c334c3d6c..bbb9dae04 100644 --- a/docs/api/chat.md +++ b/docs/api/chat.md @@ -7,6 +7,9 @@ | POST | `/knowledge-chat/:session_id` | 基于知识库的问答 | | POST | `/agent-chat/:session_id` | 基于 Agent 的智能问答 | | POST | `/knowledge-search` | 基于知识库的搜索知识 | +| GET | `/sessions/:session_id/messages/:message_id/suggestions` | 获取已生成的回答后推荐 | +| POST | `/sessions/:session_id/messages/:message_id/suggestions` | 确保生成或换一批推荐 | +| POST | `/sessions/:session_id/suggestion-events` | 上报曝光、点击、关闭事件 | ## POST `/knowledge-chat/:session_id` - 基于知识库的问答 @@ -26,6 +29,7 @@ | `enable_memory` | bool | 否 | 是否启用记忆功能 | | `images` | object[] | 否 | 附带的图片(base64 格式),需要 Agent 启用图片上传 | | `channel` | string | 否 | 来源渠道标识:`web`、`api`、`im`、`browser_extension` | +| `suggestion_attribution` | object | 否 | 用户从推荐问题发起本轮时传入 `{suggestion_set_id, question_id}`;服务端会校验归属 | **请求**: @@ -76,6 +80,33 @@ Agent 模式支持更智能的问答,包括工具调用、网络搜索、多 | `enable_memory` | bool | 否 | 是否启用记忆功能 | | `images` | object[] | 否 | 附带的图片(base64 格式),需要 Agent 启用图片上传 | | `channel` | string | 否 | 来源渠道标识:`web`、`api`、`im`、`browser_extension` | +| `suggestion_attribution` | object | 否 | 用户从推荐问题发起本轮时传入 `{suggestion_set_id, question_id}`;服务端会校验归属 | + +## 回答后推荐问题 + +回答主消息完成后,服务端会异步生成推荐问题,不阻塞 SSE 的 `complete`/`done` 事件。生成结果按“租户、助手消息、位置、配置快照、语言”持久化并去重。 + +```http +POST /api/v1/sessions/{session_id}/messages/{message_id}/suggestions +Content-Type: application/json + +{"regenerate": false} +``` + +状态包括 `generating`、`ready`、`suppressed`、`failed`。`ready` 时的每个问题都有稳定 `id`,点击后应先上报事件,并在下一次聊天请求中携带 `suggestion_attribution`。 + +```http +POST /api/v1/sessions/{session_id}/suggestion-events +Content-Type: application/json + +{ + "suggestion_set_id": "...", + "question_id": "...", + "event_type": "click" +} +``` + +网页嵌入提供同构接口:`/api/v1/embed/{channel_id}/sessions/{session_id}/...`,继续使用嵌入令牌和 `X-Embed-Session`。 **mentioned_items 结构**: diff --git a/docs/docs.go b/docs/docs.go index cf62b806f..250faf25d 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -10674,6 +10674,111 @@ const docTemplate = `{ } } }, + "/sessions/{session_id}/messages/{message_id}/suggestions": { + "get": { + "security": [ + { + "Bearer": [] + }, + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "会话" + ], + "summary": "获取回答后推荐问题", + "parameters": [ + { + "type": "string", + "description": "会话 ID", + "name": "session_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "助手消息 ID", + "name": "message_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "post": { + "security": [ + { + "Bearer": [] + }, + { + "ApiKeyAuth": [] + } + ], + "description": "对已完成的助手消息异步生成或重新生成推荐问题;相同配置快照会复用持久化结果", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "会话" + ], + "summary": "确保生成回答后推荐问题", + "parameters": [ + { + "type": "string", + "description": "会话 ID", + "name": "session_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "助手消息 ID", + "name": "message_id", + "in": "path", + "required": true + }, + { + "description": "生成选项", + "name": "request", + "in": "body", + "schema": { + "$ref": "#/definitions/internal_handler.EnsureMessageSuggestionsRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "202": { + "description": "Accepted", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/sessions/{session_id}/pin": { "post": { "security": [ @@ -10774,6 +10879,49 @@ const docTemplate = `{ } } }, + "/sessions/{session_id}/suggestion-events": { + "post": { + "security": [ + { + "Bearer": [] + }, + { + "ApiKeyAuth": [] + } + ], + "description": "记录曝光、点击或关闭事件", + "consumes": [ + "application/json" + ], + "tags": [ + "会话" + ], + "summary": "上报推荐问题事件", + "parameters": [ + { + "type": "string", + "description": "会话 ID", + "name": "session_id", + "in": "path", + "required": true + }, + { + "description": "事件", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handler.SuggestionEventRequest" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, "/sessions/{session_id}/title": { "post": { "security": [ @@ -14481,6 +14629,14 @@ const docTemplate = `{ "description": "Dedicated chat model ID for the query-understanding (rewrite + intent) step.\nWhen empty, the main conversation ModelID is used as a fallback.", "type": "string" }, + "question_suggestions": { + "description": "===== Conversation Question Suggestions =====\nQuestionSuggestions owns both the static/knowledge-backed prompts shown\nbefore the first user turn and the contextual follow-up questions shown\nafter a completed assistant answer.", + "allOf": [ + { + "$ref": "#/definitions/github_com_Tencent_WeKnora_internal_types.QuestionSuggestionConfig" + } + ] + }, "rerank_model_id": { "description": "ReRank model ID for retrieval", "type": "string" @@ -14520,13 +14676,6 @@ const docTemplate = `{ "description": "===== Skills Settings (only for smart-reasoning mode) =====\nSkills selection mode: \"all\" = all preloaded skills, \"selected\" = specific skills, \"none\" = no skills", "type": "string" }, - "suggested_prompts": { - "description": "===== Suggested Prompts =====\n推荐问题列表,用于在前端对话面板展示快捷提问", - "type": "array", - "items": { - "type": "string" - } - }, "supported_file_types": { "description": "===== File Type Restriction Settings =====\nSupported file types for this agent (e.g., [\"csv\", \"xlsx\", \"xls\"])\nEmpty means all file types are supported\nWhen set, only files with matching extensions can be used with this agent", "type": "array", @@ -14921,6 +15070,47 @@ const docTemplate = `{ } } }, + "github_com_Tencent_WeKnora_internal_types.FollowUpSuggestionConfig": { + "type": "object", + "properties": { + "additional_instruction": { + "type": "string" + }, + "allow_regenerate": { + "type": "boolean" + }, + "categories": { + "type": "array", + "items": { + "type": "string" + } + }, + "count": { + "type": "integer" + }, + "enabled": { + "type": "boolean" + }, + "knowledge_fallback": { + "type": "boolean" + }, + "max_context_turns": { + "type": "integer" + }, + "mode": { + "type": "string" + }, + "model_id": { + "type": "string" + }, + "suppress_on_fallback": { + "type": "boolean" + }, + "suppress_when_answer_asks_question": { + "type": "boolean" + } + } + }, "github_com_Tencent_WeKnora_internal_types.GraphNode": { "type": "object", "properties": { @@ -16123,6 +16313,10 @@ const docTemplate = `{ "description": "Agent total execution duration in milliseconds (from query start to answer start)", "type": "integer" }, + "agent_id": { + "description": "AgentID is the agent used for this individual assistant turn. Unlike the\nsession's last_request_state it remains stable when users switch agents.", + "type": "string" + }, "agent_steps": { "description": "Agent execution steps (only for assistant messages generated by agent)\nThis contains the detailed reasoning process and tool calls made by the agent\nStored for user history display, but NOT included in LLM context to avoid redundancy", "type": "array", @@ -16194,6 +16388,10 @@ const docTemplate = `{ "$ref": "#/definitions/github_com_Tencent_WeKnora_internal_types.MentionedItem" } }, + "model_id": { + "description": "ModelID is the requested/effective chat model binding captured for this\nturn. It is useful for reproducibility and suggestion generation.", + "type": "string" + }, "request_id": { "description": "Request identifier for tracking API requests", "type": "string" @@ -16784,6 +16982,17 @@ const docTemplate = `{ } } }, + "github_com_Tencent_WeKnora_internal_types.QuestionSuggestionConfig": { + "type": "object", + "properties": { + "follow_ups": { + "$ref": "#/definitions/github_com_Tencent_WeKnora_internal_types.FollowUpSuggestionConfig" + }, + "starters": { + "$ref": "#/definitions/github_com_Tencent_WeKnora_internal_types.StarterSuggestionConfig" + } + } + }, "github_com_Tencent_WeKnora_internal_types.QueueStat": { "type": "object", "properties": { @@ -17424,6 +17633,26 @@ const docTemplate = `{ } } }, + "github_com_Tencent_WeKnora_internal_types.StarterSuggestionConfig": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "enabled": { + "type": "boolean" + }, + "items": { + "type": "array", + "items": { + "type": "string" + } + }, + "mode": { + "type": "string" + } + } + }, "github_com_Tencent_WeKnora_internal_types.StorageConfig": { "type": "object", "properties": { @@ -17536,6 +17765,17 @@ const docTemplate = `{ } } }, + "github_com_Tencent_WeKnora_internal_types.SuggestionAttribution": { + "type": "object", + "properties": { + "question_id": { + "type": "string" + }, + "suggestion_set_id": { + "type": "string" + } + } + }, "github_com_Tencent_WeKnora_internal_types.SyncLog": { "type": "object", "properties": { @@ -19060,6 +19300,14 @@ const docTemplate = `{ } } }, + "internal_handler.EnsureMessageSuggestionsRequest": { + "type": "object", + "properties": { + "regenerate": { + "type": "boolean" + } + } + }, "internal_handler.EvaluationRequest": { "type": "object", "properties": { @@ -20015,6 +20263,24 @@ const docTemplate = `{ } } }, + "internal_handler.SuggestionEventRequest": { + "type": "object", + "required": [ + "event_type", + "suggestion_set_id" + ], + "properties": { + "event_type": { + "type": "string" + }, + "question_id": { + "type": "string" + }, + "suggestion_set_id": { + "type": "string" + } + } + }, "internal_handler.TestProviderRequest": { "type": "object", "required": [ @@ -20533,6 +20799,9 @@ const docTemplate = `{ "type": "string" } }, + "suggestion_attribution": { + "$ref": "#/definitions/github_com_Tencent_WeKnora_internal_types.SuggestionAttribution" + }, "summary_model_id": { "description": "Optional summary model ID for this request (overrides session default)", "type": "string" diff --git a/docs/swagger.json b/docs/swagger.json index b7d3d209a..7f621f77b 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -10667,6 +10667,111 @@ } } }, + "/sessions/{session_id}/messages/{message_id}/suggestions": { + "get": { + "security": [ + { + "Bearer": [] + }, + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "会话" + ], + "summary": "获取回答后推荐问题", + "parameters": [ + { + "type": "string", + "description": "会话 ID", + "name": "session_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "助手消息 ID", + "name": "message_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "post": { + "security": [ + { + "Bearer": [] + }, + { + "ApiKeyAuth": [] + } + ], + "description": "对已完成的助手消息异步生成或重新生成推荐问题;相同配置快照会复用持久化结果", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "会话" + ], + "summary": "确保生成回答后推荐问题", + "parameters": [ + { + "type": "string", + "description": "会话 ID", + "name": "session_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "助手消息 ID", + "name": "message_id", + "in": "path", + "required": true + }, + { + "description": "生成选项", + "name": "request", + "in": "body", + "schema": { + "$ref": "#/definitions/internal_handler.EnsureMessageSuggestionsRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "202": { + "description": "Accepted", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/sessions/{session_id}/pin": { "post": { "security": [ @@ -10767,6 +10872,49 @@ } } }, + "/sessions/{session_id}/suggestion-events": { + "post": { + "security": [ + { + "Bearer": [] + }, + { + "ApiKeyAuth": [] + } + ], + "description": "记录曝光、点击或关闭事件", + "consumes": [ + "application/json" + ], + "tags": [ + "会话" + ], + "summary": "上报推荐问题事件", + "parameters": [ + { + "type": "string", + "description": "会话 ID", + "name": "session_id", + "in": "path", + "required": true + }, + { + "description": "事件", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handler.SuggestionEventRequest" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, "/sessions/{session_id}/title": { "post": { "security": [ @@ -14474,6 +14622,14 @@ "description": "Dedicated chat model ID for the query-understanding (rewrite + intent) step.\nWhen empty, the main conversation ModelID is used as a fallback.", "type": "string" }, + "question_suggestions": { + "description": "===== Conversation Question Suggestions =====\nQuestionSuggestions owns both the static/knowledge-backed prompts shown\nbefore the first user turn and the contextual follow-up questions shown\nafter a completed assistant answer.", + "allOf": [ + { + "$ref": "#/definitions/github_com_Tencent_WeKnora_internal_types.QuestionSuggestionConfig" + } + ] + }, "rerank_model_id": { "description": "ReRank model ID for retrieval", "type": "string" @@ -14513,13 +14669,6 @@ "description": "===== Skills Settings (only for smart-reasoning mode) =====\nSkills selection mode: \"all\" = all preloaded skills, \"selected\" = specific skills, \"none\" = no skills", "type": "string" }, - "suggested_prompts": { - "description": "===== Suggested Prompts =====\n推荐问题列表,用于在前端对话面板展示快捷提问", - "type": "array", - "items": { - "type": "string" - } - }, "supported_file_types": { "description": "===== File Type Restriction Settings =====\nSupported file types for this agent (e.g., [\"csv\", \"xlsx\", \"xls\"])\nEmpty means all file types are supported\nWhen set, only files with matching extensions can be used with this agent", "type": "array", @@ -14914,6 +15063,47 @@ } } }, + "github_com_Tencent_WeKnora_internal_types.FollowUpSuggestionConfig": { + "type": "object", + "properties": { + "additional_instruction": { + "type": "string" + }, + "allow_regenerate": { + "type": "boolean" + }, + "categories": { + "type": "array", + "items": { + "type": "string" + } + }, + "count": { + "type": "integer" + }, + "enabled": { + "type": "boolean" + }, + "knowledge_fallback": { + "type": "boolean" + }, + "max_context_turns": { + "type": "integer" + }, + "mode": { + "type": "string" + }, + "model_id": { + "type": "string" + }, + "suppress_on_fallback": { + "type": "boolean" + }, + "suppress_when_answer_asks_question": { + "type": "boolean" + } + } + }, "github_com_Tencent_WeKnora_internal_types.GraphNode": { "type": "object", "properties": { @@ -16116,6 +16306,10 @@ "description": "Agent total execution duration in milliseconds (from query start to answer start)", "type": "integer" }, + "agent_id": { + "description": "AgentID is the agent used for this individual assistant turn. Unlike the\nsession's last_request_state it remains stable when users switch agents.", + "type": "string" + }, "agent_steps": { "description": "Agent execution steps (only for assistant messages generated by agent)\nThis contains the detailed reasoning process and tool calls made by the agent\nStored for user history display, but NOT included in LLM context to avoid redundancy", "type": "array", @@ -16187,6 +16381,10 @@ "$ref": "#/definitions/github_com_Tencent_WeKnora_internal_types.MentionedItem" } }, + "model_id": { + "description": "ModelID is the requested/effective chat model binding captured for this\nturn. It is useful for reproducibility and suggestion generation.", + "type": "string" + }, "request_id": { "description": "Request identifier for tracking API requests", "type": "string" @@ -16777,6 +16975,17 @@ } } }, + "github_com_Tencent_WeKnora_internal_types.QuestionSuggestionConfig": { + "type": "object", + "properties": { + "follow_ups": { + "$ref": "#/definitions/github_com_Tencent_WeKnora_internal_types.FollowUpSuggestionConfig" + }, + "starters": { + "$ref": "#/definitions/github_com_Tencent_WeKnora_internal_types.StarterSuggestionConfig" + } + } + }, "github_com_Tencent_WeKnora_internal_types.QueueStat": { "type": "object", "properties": { @@ -17417,6 +17626,26 @@ } } }, + "github_com_Tencent_WeKnora_internal_types.StarterSuggestionConfig": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "enabled": { + "type": "boolean" + }, + "items": { + "type": "array", + "items": { + "type": "string" + } + }, + "mode": { + "type": "string" + } + } + }, "github_com_Tencent_WeKnora_internal_types.StorageConfig": { "type": "object", "properties": { @@ -17529,6 +17758,17 @@ } } }, + "github_com_Tencent_WeKnora_internal_types.SuggestionAttribution": { + "type": "object", + "properties": { + "question_id": { + "type": "string" + }, + "suggestion_set_id": { + "type": "string" + } + } + }, "github_com_Tencent_WeKnora_internal_types.SyncLog": { "type": "object", "properties": { @@ -19053,6 +19293,14 @@ } } }, + "internal_handler.EnsureMessageSuggestionsRequest": { + "type": "object", + "properties": { + "regenerate": { + "type": "boolean" + } + } + }, "internal_handler.EvaluationRequest": { "type": "object", "properties": { @@ -20008,6 +20256,24 @@ } } }, + "internal_handler.SuggestionEventRequest": { + "type": "object", + "required": [ + "event_type", + "suggestion_set_id" + ], + "properties": { + "event_type": { + "type": "string" + }, + "question_id": { + "type": "string" + }, + "suggestion_set_id": { + "type": "string" + } + } + }, "internal_handler.TestProviderRequest": { "type": "object", "required": [ @@ -20526,6 +20792,9 @@ "type": "string" } }, + "suggestion_attribution": { + "$ref": "#/definitions/github_com_Tencent_WeKnora_internal_types.SuggestionAttribution" + }, "summary_model_id": { "description": "Optional summary model ID for this request (overrides session default)", "type": "string" diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 6e9accfa0..73bd2bda0 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -713,6 +713,14 @@ definitions: Dedicated chat model ID for the query-understanding (rewrite + intent) step. When empty, the main conversation ModelID is used as a fallback. type: string + question_suggestions: + allOf: + - $ref: '#/definitions/github_com_Tencent_WeKnora_internal_types.QuestionSuggestionConfig' + description: |- + ===== Conversation Question Suggestions ===== + QuestionSuggestions owns both the static/knowledge-backed prompts shown + before the first user turn and the contextual follow-up questions shown + after a completed assistant answer. rerank_model_id: description: ReRank model ID for retrieval type: string @@ -747,13 +755,6 @@ definitions: ===== Skills Settings (only for smart-reasoning mode) ===== Skills selection mode: "all" = all preloaded skills, "selected" = specific skills, "none" = no skills type: string - suggested_prompts: - description: |- - ===== Suggested Prompts ===== - 推荐问题列表,用于在前端对话面板展示快捷提问 - items: - type: string - type: array supported_file_types: description: |- ===== File Type Restriction Settings ===== @@ -1045,6 +1046,33 @@ definitions: required: - query_text type: object + github_com_Tencent_WeKnora_internal_types.FollowUpSuggestionConfig: + properties: + additional_instruction: + type: string + allow_regenerate: + type: boolean + categories: + items: + type: string + type: array + count: + type: integer + enabled: + type: boolean + knowledge_fallback: + type: boolean + max_context_turns: + type: integer + mode: + type: string + model_id: + type: string + suppress_on_fallback: + type: boolean + suppress_when_answer_asks_question: + type: boolean + type: object github_com_Tencent_WeKnora_internal_types.GraphNode: properties: attributes: @@ -1938,6 +1966,11 @@ definitions: description: Agent total execution duration in milliseconds (from query start to answer start) type: integer + agent_id: + description: |- + AgentID is the agent used for this individual assistant turn. Unlike the + session's last_request_state it remains stable when users switch agents. + type: string agent_steps: description: |- Agent execution steps (only for assistant messages generated by agent) @@ -1997,6 +2030,11 @@ definitions: items: $ref: '#/definitions/github_com_Tencent_WeKnora_internal_types.MentionedItem' type: array + model_id: + description: |- + ModelID is the requested/effective chat model binding captured for this + turn. It is useful for reproducibility and suggestion generation. + type: string request_id: description: Request identifier for tracking API requests type: string @@ -2447,6 +2485,13 @@ definitions: 10)' type: integer type: object + github_com_Tencent_WeKnora_internal_types.QuestionSuggestionConfig: + properties: + follow_ups: + $ref: '#/definitions/github_com_Tencent_WeKnora_internal_types.FollowUpSuggestionConfig' + starters: + $ref: '#/definitions/github_com_Tencent_WeKnora_internal_types.StarterSuggestionConfig' + type: object github_com_Tencent_WeKnora_internal_types.QueueStat: properties: active: @@ -2928,6 +2973,19 @@ definitions: - organization_id - permission type: object + github_com_Tencent_WeKnora_internal_types.StarterSuggestionConfig: + properties: + count: + type: integer + enabled: + type: boolean + items: + items: + type: string + type: array + mode: + type: string + type: object github_com_Tencent_WeKnora_internal_types.StorageConfig: properties: app_id: @@ -3006,6 +3064,13 @@ definitions: required: - invite_code type: object + github_com_Tencent_WeKnora_internal_types.SuggestionAttribution: + properties: + question_id: + type: string + suggestion_set_id: + type: string + type: object github_com_Tencent_WeKnora_internal_types.SyncLog: properties: created_at: @@ -4179,6 +4244,11 @@ definitions: type: integer type: array type: object + internal_handler.EnsureMessageSuggestionsRequest: + properties: + regenerate: + type: boolean + type: object internal_handler.EvaluationRequest: properties: chat_id: @@ -4848,6 +4918,18 @@ definitions: description: '"local", "minio", "cos", "tos", "s3", "oss", "ks3"' type: string type: object + internal_handler.SuggestionEventRequest: + properties: + event_type: + type: string + question_id: + type: string + suggestion_set_id: + type: string + required: + - event_type + - suggestion_set_id + type: object internal_handler.TestProviderRequest: properties: parameters: @@ -5213,6 +5295,8 @@ definitions: items: type: string type: array + suggestion_attribution: + $ref: '#/definitions/github_com_Tencent_WeKnora_internal_types.SuggestionAttribution' summary_model_id: description: Optional summary model ID for this request (overrides session default) @@ -12017,6 +12101,72 @@ paths: summary: 知识问答 tags: - 问答 + /sessions/{session_id}/messages/{message_id}/suggestions: + get: + parameters: + - description: 会话 ID + in: path + name: session_id + required: true + type: string + - description: 助手消息 ID + in: path + name: message_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + - ApiKeyAuth: [] + summary: 获取回答后推荐问题 + tags: + - 会话 + post: + consumes: + - application/json + description: 对已完成的助手消息异步生成或重新生成推荐问题;相同配置快照会复用持久化结果 + parameters: + - description: 会话 ID + in: path + name: session_id + required: true + type: string + - description: 助手消息 ID + in: path + name: message_id + required: true + type: string + - description: 生成选项 + in: body + name: request + schema: + $ref: '#/definitions/internal_handler.EnsureMessageSuggestionsRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "202": + description: Accepted + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + - ApiKeyAuth: [] + summary: 确保生成回答后推荐问题 + tags: + - 会话 /sessions/{session_id}/pin: post: description: 将指定会话置顶(用户维度) @@ -12079,6 +12229,32 @@ paths: summary: 停止生成 tags: - 问答 + /sessions/{session_id}/suggestion-events: + post: + consumes: + - application/json + description: 记录曝光、点击或关闭事件 + parameters: + - description: 会话 ID + in: path + name: session_id + required: true + type: string + - description: 事件 + in: body + name: request + required: true + schema: + $ref: '#/definitions/internal_handler.SuggestionEventRequest' + responses: + "204": + description: No Content + security: + - Bearer: [] + - ApiKeyAuth: [] + summary: 上报推荐问题事件 + tags: + - 会话 /sessions/{session_id}/title: post: consumes: diff --git a/frontend/src/api/agent/index.ts b/frontend/src/api/agent/index.ts index 0058f3430..4a9dad451 100644 --- a/frontend/src/api/agent/index.ts +++ b/frontend/src/api/agent/index.ts @@ -8,6 +8,28 @@ import { get, post, put, del } from "../../utils/request"; // 'custom' : 完全自定义(不应用预设) export type AgentType = 'rag-qa' | 'wiki-qa' | 'hybrid-rag-wiki' | 'data-analysis' | 'custom'; +export interface QuestionSuggestionConfig { + starters: { + enabled: boolean; + mode: 'curated' | 'knowledge' | 'hybrid'; + items: string[]; + count: number; + }; + follow_ups: { + enabled: boolean; + mode: 'generated' | 'knowledge' | 'hybrid'; + count: number; + model_id?: string; + additional_instruction?: string; + categories: Array<'clarify' | 'deepen' | 'action'>; + max_context_turns: number; + suppress_on_fallback: boolean; + suppress_when_answer_asks_question: boolean; + knowledge_fallback: boolean; + allow_regenerate: boolean; + }; +} + export interface CustomAgentConfig { // ===== 基础设置 ===== agent_mode?: 'quick-answer' | 'smart-reasoning'; // 运行模式:quick-answer=RAG模式, smart-reasoning=ReAct Agent模式 @@ -92,7 +114,7 @@ export interface CustomAgentConfig { // ===== 已废弃字段(保留兼容)===== welcome_message?: string; - suggested_prompts?: string[]; + question_suggestions?: QuestionSuggestionConfig; } // 智能体 diff --git a/frontend/src/api/chat/streame.ts b/frontend/src/api/chat/streame.ts index 1d08b9006..879ef57cb 100644 --- a/frontend/src/api/chat/streame.ts +++ b/frontend/src/api/chat/streame.ts @@ -36,7 +36,7 @@ export function useStream() { let renderTimer: number | null = null // 启动流式请求 - const startStream = async (params: { session_id: any; query: any; knowledge_base_ids?: string[]; knowledge_ids?: string[]; tag_ids?: string[]; agent_enabled?: boolean; agent_id?: string; web_search_enabled?: boolean; enable_memory?: boolean; summary_model_id?: string; mcp_service_ids?: string[]; skill_names?: string[]; mentioned_items?: Array<{id: string; name: string; type: string; kb_type?: string; kb_id?: string; kb_name?: string; service_id?: string; skill_name?: string}>; images?: Array<{data: string}>; attachment_uploads?: Array<{data: string; file_name: string; file_size: number}>; method: string; url: string; embed_token?: string; embed_session_sig?: string; embed_visitor_id?: string }) => { + const startStream = async (params: { session_id: any; query: any; knowledge_base_ids?: string[]; knowledge_ids?: string[]; tag_ids?: string[]; agent_enabled?: boolean; agent_id?: string; web_search_enabled?: boolean; enable_memory?: boolean; summary_model_id?: string; mcp_service_ids?: string[]; skill_names?: string[]; mentioned_items?: Array<{id: string; name: string; type: string; kb_type?: string; kb_id?: string; kb_name?: string; service_id?: string; skill_name?: string}>; images?: Array<{data: string}>; attachment_uploads?: Array<{data: string; file_name: string; file_size: number}>; suggestion_attribution?: { suggestion_set_id: string; question_id: string }; method: string; url: string; embed_token?: string; embed_session_sig?: string; embed_visitor_id?: string }) => { const myGeneration = ++streamGeneration // 重置状态 output.value = ''; @@ -133,6 +133,9 @@ export function useStream() { if (params.attachment_uploads !== undefined && params.attachment_uploads.length > 0) { postBody.attachment_uploads = params.attachment_uploads; } + if (params.suggestion_attribution) { + postBody.suggestion_attribution = params.suggestion_attribution; + } postBody.channel = embedToken ? "embed" : "web"; lastStreamRequest.value = { diff --git a/frontend/src/api/embed/index.ts b/frontend/src/api/embed/index.ts index c9adb3995..bf924bfe4 100644 --- a/frontend/src/api/embed/index.ts +++ b/frontend/src/api/embed/index.ts @@ -170,6 +170,20 @@ export interface SuggestedQuestion { source?: string } +export interface EmbedMessageSuggestionItem { + id: string + text: string + category?: string + source: string +} + +export interface EmbedMessageSuggestionSet { + id: string + status: 'generating' | 'ready' | 'suppressed' | 'failed' + allow_regenerate: boolean + questions: EmbedMessageSuggestionItem[] +} + export async function getEmbedChunkById(channelId: string, token: string, chunkId: string) { return get<{ success: boolean; data: { content?: string } }>( `/api/v1/embed/${channelId}/chunks/${chunkId}`, @@ -184,6 +198,53 @@ export async function getEmbedSuggestedQuestions(channelId: string, token: strin ) } +export async function ensureEmbedMessageSuggestions( + channelId: string, + token: string, + sessionId: string, + messageId: string, + sessionSig: string, + visitorId: string, + regenerate = false, +) { + return post<{ success: boolean; data: EmbedMessageSuggestionSet }>( + `/api/v1/embed/${channelId}/sessions/${sessionId}/messages/${messageId}/suggestions`, + { regenerate }, + { headers: embedSessionHeaders(token, sessionSig, visitorId) }, + ) +} + +export async function getEmbedMessageSuggestions( + channelId: string, + token: string, + sessionId: string, + messageId: string, + sessionSig: string, + visitorId: string, +) { + return get<{ success: boolean; data: EmbedMessageSuggestionSet }>( + `/api/v1/embed/${channelId}/sessions/${sessionId}/messages/${messageId}/suggestions`, + { headers: embedSessionHeaders(token, sessionSig, visitorId) }, + ) +} + +export async function recordEmbedMessageSuggestionEvent( + channelId: string, + token: string, + sessionId: string, + sessionSig: string, + visitorId: string, + suggestionSetId: string, + eventType: 'impression' | 'click' | 'dismiss', + questionId = '', +) { + return post( + `/api/v1/embed/${channelId}/sessions/${sessionId}/suggestion-events`, + { suggestion_set_id: suggestionSetId, question_id: questionId, event_type: eventType }, + { headers: embedSessionHeaders(token, sessionSig, visitorId) }, + ) +} + export async function getEmbedConfig(channelId: string, token: string) { return get<{ success: boolean; data: EmbedChannelPublicConfig }>( `/api/v1/embed/${channelId}/config`, diff --git a/frontend/src/api/message-suggestion.ts b/frontend/src/api/message-suggestion.ts new file mode 100644 index 000000000..202fbecd8 --- /dev/null +++ b/frontend/src/api/message-suggestion.ts @@ -0,0 +1,45 @@ +import { get, post } from '@/utils/request' + +export interface MessageSuggestionItem { + id: string + text: string + category?: 'clarify' | 'deepen' | 'action' + source: 'model' | 'faq' | 'document' | 'wiki' | string + knowledge_base_ids?: string[] +} + +export interface MessageSuggestionSet { + id: string + session_id: string + assistant_message_id: string + status: 'generating' | 'ready' | 'suppressed' | 'failed' + allow_regenerate: boolean + suppression_reason?: string + questions: MessageSuggestionItem[] + generated_at?: string +} + +export function ensureMessageSuggestions(sessionId: string, messageId: string, regenerate = false) { + return post<{ data: MessageSuggestionSet }>( + `/api/v1/sessions/${sessionId}/messages/${messageId}/suggestions`, + { regenerate }, + ) +} + +export function getMessageSuggestions(sessionId: string, messageId: string) { + return get<{ data: MessageSuggestionSet }>( + `/api/v1/sessions/${sessionId}/messages/${messageId}/suggestions`, + ) +} + +export function recordMessageSuggestionEvent( + sessionId: string, + suggestionSetId: string, + eventType: 'impression' | 'click' | 'dismiss', + questionId = '', +) { + return post( + `/api/v1/sessions/${sessionId}/suggestion-events`, + { suggestion_set_id: suggestionSetId, question_id: questionId, event_type: eventType }, + ) +} diff --git a/frontend/src/components/chat/FollowUpSuggestions.vue b/frontend/src/components/chat/FollowUpSuggestions.vue new file mode 100644 index 000000000..b1ea36fa1 --- /dev/null +++ b/frontend/src/components/chat/FollowUpSuggestions.vue @@ -0,0 +1,144 @@ + + + + + diff --git a/frontend/src/composables/useChatStreamHandler.ts b/frontend/src/composables/useChatStreamHandler.ts index f3e53b24f..e3e1619b5 100644 --- a/frontend/src/composables/useChatStreamHandler.ts +++ b/frontend/src/composables/useChatStreamHandler.ts @@ -13,6 +13,7 @@ export interface UseChatStreamHandlerOptions { isAgentStreamSession: () => boolean scrollToBottom: (force?: boolean) => void onReplyComplete?: (content: string) => void + onTurnComplete?: (message: ChatMessage) => void onError?: (message: string) => void /** Main chat: keep the last incomplete message reactive for continue-stream. */ preserveIncompleteStreamReactive?: boolean @@ -42,6 +43,7 @@ export function useChatStreamHandler(options: UseChatStreamHandlerOptions) { isAgentStreamSession, scrollToBottom, onReplyComplete, + onTurnComplete, onError, preserveIncompleteStreamReactive = false, isFirstEnter, @@ -808,6 +810,7 @@ export function useChatStreamHandler(options: UseChatStreamHandlerOptions) { isReplying.value = false message.is_completed = true onReplyComplete?.(String(message.content || '')) + onTurnComplete?.(message) fullContent.value = '' currentAssistantMessageId.value = '' if (message.agentEventStream) { @@ -1003,6 +1006,10 @@ export function useChatStreamHandler(options: UseChatStreamHandlerOptions) { currentAssistantMessageId.value = '' } updateAssistantSession(obj) + if (data.done) { + const completed = resolveActiveAssistantMessage(data) || obj + onTurnComplete?.(completed) + } } return { diff --git a/frontend/src/composables/useEmbedChatSession.ts b/frontend/src/composables/useEmbedChatSession.ts index 0af2d8e54..e96075c0e 100644 --- a/frontend/src/composables/useEmbedChatSession.ts +++ b/frontend/src/composables/useEmbedChatSession.ts @@ -35,6 +35,8 @@ export function useEmbedChatSession(options: { hostContext?: Ref> onMessagesChange?: (has: boolean) => void onSessionTitle?: (title: string) => void + onTurnComplete?: (message: Record) => void + onMessagesLoaded?: (messages: Record[]) => void }) { const { t } = useI18n() const { onChunk, error, startStream, stopStream } = useStream() @@ -59,6 +61,7 @@ export function useEmbedChatSession(options: { const hasMoreHistory = ref(true) const created_at = ref('') const fullContent = ref('') + let pendingSuggestionAttribution: { suggestion_set_id: string; question_id: string } | null = null const scrollContainer = ref(null) const userHasScrolledUp = ref(false) const SCROLL_BOTTOM_THRESHOLD = 80 @@ -130,6 +133,8 @@ export function useEmbedChatSession(options: { isAgentStreamSession, scrollToBottom, onReplyComplete: notifyEmbedReceived, + onTurnComplete: options.onTurnComplete, + onAfterMsgList: () => options.onMessagesLoaded?.(messagesList), onError: embedToast, isFirstEnter, scrollContainer, @@ -298,6 +303,8 @@ export function useEmbedChatSession(options: { ? `/api/v1/embed/${options.channelId}/agent-chat` : `/api/v1/embed/${options.channelId}/knowledge-chat` + const suggestionAttribution = pendingSuggestionAttribution + pendingSuggestionAttribution = null await startStream({ session_id: options.sessionId.value, knowledge_base_ids: options.kbIds, @@ -312,6 +319,7 @@ export function useEmbedChatSession(options: { images: imageAttachments.length > 0 ? imageAttachments : undefined, attachment_uploads: attachmentUploads.length > 0 ? attachmentUploads : undefined, query: outboundQuery, + suggestion_attribution: suggestionAttribution || undefined, method: 'POST', url: endpoint, embed_token: options.token, @@ -391,5 +399,8 @@ export function useEmbedChatSession(options: { onClickScrollToBottom, sendMsg, handleStopGeneration, + setSuggestionAttribution: (suggestionSetId: string, questionId: string) => { + pendingSuggestionAttribution = { suggestion_set_id: suggestionSetId, question_id: questionId } + }, } } diff --git a/frontend/src/i18n/embed.ts b/frontend/src/i18n/embed.ts index a28f63223..2bf31d71c 100644 --- a/frontend/src/i18n/embed.ts +++ b/frontend/src/i18n/embed.ts @@ -77,6 +77,8 @@ const messages = { "newChat": "新对话", "suggestedQuestions": "你可以这样问我", "suggestedQuestionsLoading": "正在加载推荐问题...", + "followUpQuestions": "接下来可以继续问", + "refreshSuggestedQuestions": "换一批推荐问题", "inputPlaceholder": "请输入您的消息...", "send": "发送", "thinking": "思考中...", @@ -550,6 +552,8 @@ const messages = { "newChat": "New Chat", "suggestedQuestions": "You can ask me", "suggestedQuestionsLoading": "Loading suggestions...", + "followUpQuestions": "Continue with a follow-up", + "refreshSuggestedQuestions": "Refresh suggestions", "inputPlaceholder": "Enter your message...", "send": "Send", "thinking": "Thinking...", @@ -1008,6 +1012,8 @@ const koEmbedPublish = { }, chat: { suggestedQuestions: '이렇게 물어보세요', + followUpQuestions: '이어서 질문해 보세요', + refreshSuggestedQuestions: '추천 질문 새로고침', imageTooMany: '이미지는 최대 5장까지 업로드할 수 있습니다', imageTypeSizeError: 'JPG/PNG/GIF/WEBP만 지원하며, 각 파일은 10MB 이하여야 합니다', imageReadFailed: '이미지를 읽지 못했습니다', @@ -1091,6 +1097,8 @@ const ruEmbedPublish = { }, chat: { suggestedQuestions: 'Вы можете спросить так', + followUpQuestions: 'Продолжите уточняющим вопросом', + refreshSuggestedQuestions: 'Обновить предложения', imageTooMany: 'Можно загрузить не более 5 изображений', imageTypeSizeError: 'Поддерживаются только JPG/PNG/GIF/WEBP, каждый файл до 10 МБ', imageReadFailed: 'Не удалось прочитать изображение', diff --git a/frontend/src/i18n/locales/en-US.ts b/frontend/src/i18n/locales/en-US.ts index 43901927b..4a59c1972 100755 --- a/frontend/src/i18n/locales/en-US.ts +++ b/frontend/src/i18n/locales/en-US.ts @@ -3081,6 +3081,7 @@ export default { title: 'Chat', newChat: 'New Chat', suggestedQuestions: 'You can ask me', + followUpQuestions: 'Continue with a follow-up', suggestedQuestionsLoading: 'Loading suggestions...', refreshSuggestedQuestions: 'Refresh suggestions', inputPlaceholder: 'Enter your message...', @@ -5487,6 +5488,41 @@ export default { capability: 'Extensions', integration: 'Publish & Integrations', }, + questionSuggestions: { + navLabel: 'Question suggestions', + title: 'Conversation question suggestions', + description: 'Configure starters and contextual follow-ups in one agent-owned policy. Channels may hide them but cannot override the policy.', + startersTitle: 'Conversation starters', + followUpsTitle: 'After-answer follow-ups', + enableStarters: 'Show starter questions', + enableStartersDesc: 'Shown before the first user message from curated, knowledge, or mixed sources.', + enableFollowUps: 'Generate follow-up questions', + enableFollowUpsDesc: 'Generated asynchronously after every completed answer. Enabling this adds model usage.', + sourceMode: 'Source mode', + count: 'Question count', + curatedItems: 'Curated questions', + curatedItemsDesc: 'Used for starters and prioritized in hybrid mode.', + addItem: 'Add question', + model: 'Generation model', + modelDesc: 'Uses the model from the completed turn when empty.', + advancedSettings: 'Advanced generation settings', + advancedSettingsDesc: 'Context, question types, instructions, and display rules', + displayRules: 'Display and fallback rules', + contextTurns: 'Context turns', + categories: 'Question types', + instruction: 'Additional instruction', + suppressFallback: 'Hide after fallback answers', + suppressQuestion: 'Hide when the answer ends with a question', + knowledgeFallback: 'Use knowledge candidates if generation fails', + allowRegenerate: 'Allow users to regenerate', + modeCurated: 'Curated', + modeKnowledge: 'Knowledge', + modeGenerated: 'Generated', + modeHybrid: 'Hybrid', + categoryClarify: 'Clarify', + categoryDeepen: 'Deepen', + categoryAction: 'Next step', + }, placeholders: { available: 'Available variables: ', clickToInsert: '(click to insert)', diff --git a/frontend/src/i18n/locales/ko-KR.ts b/frontend/src/i18n/locales/ko-KR.ts index ca0b6ae2e..5aeda5035 100755 --- a/frontend/src/i18n/locales/ko-KR.ts +++ b/frontend/src/i18n/locales/ko-KR.ts @@ -895,6 +895,7 @@ export default { title: "대화", newChat: "새 대화", suggestedQuestions: "이렇게 물어보세요", + followUpQuestions: "이어서 질문해 보세요", suggestedQuestionsLoading: "추천 질문 로딩 중...", refreshSuggestedQuestions: "추천 질문 새로고침", inputPlaceholder: "메시지를 입력하세요...", @@ -5498,6 +5499,24 @@ export default { capability: '기능 확장', integration: '게시 및 통합', }, + questionSuggestions: { + navLabel: '질문 추천', title: '대화 질문 추천', + description: '대화 시작 질문과 답변 후 후속 질문을 에이전트 정책으로 통합 설정합니다.', + startersTitle: '시작 질문', followUpsTitle: '답변 후 후속 질문', + enableStarters: '시작 질문 표시', enableStartersDesc: '첫 질문 전에 운영 설정 또는 지식 기반 질문을 표시합니다.', + enableFollowUps: '후속 질문 생성', enableFollowUpsDesc: '완료된 답변마다 비동기로 생성하며 추가 모델 사용량이 발생합니다.', + sourceMode: '콘텐츠 소스', count: '표시 개수', curatedItems: '운영 설정 질문', + curatedItemsDesc: '혼합 모드에서 우선 표시됩니다.', addItem: '질문 추가', + model: '생성 모델', modelDesc: '비워 두면 현재 대화 모델을 사용합니다.', + advancedSettings: '고급 생성 설정', + advancedSettingsDesc: '컨텍스트, 질문 유형, 생성 지침 및 표시 규칙', + displayRules: '표시 및 폴백 규칙', + contextTurns: '컨텍스트 턴 수', categories: '질문 유형', instruction: '추가 생성 지침', + suppressFallback: '대체 답변 뒤에는 숨기기', suppressQuestion: '답변이 질문으로 끝나면 숨기기', + knowledgeFallback: '생성 실패 시 지식 후보 사용', allowRegenerate: '새 질문 묶음 허용', + modeCurated: '운영 설정', modeKnowledge: '지식 기반', modeGenerated: '모델 생성', modeHybrid: '혼합', + categoryClarify: '명확화', categoryDeepen: '심화', categoryAction: '다음 단계', + }, placeholders: { available: '사용 가능한 변수: ', clickToInsert: '(클릭하여 삽입)', diff --git a/frontend/src/i18n/locales/ru-RU.ts b/frontend/src/i18n/locales/ru-RU.ts index 4b16f73a2..c66c25d6d 100755 --- a/frontend/src/i18n/locales/ru-RU.ts +++ b/frontend/src/i18n/locales/ru-RU.ts @@ -3663,6 +3663,7 @@ export default { title: 'Диалог', newChat: 'Новый чат', suggestedQuestions: 'Вы можете спросить меня', + followUpQuestions: 'Продолжите уточняющим вопросом', suggestedQuestionsLoading: 'Загрузка предложений...', refreshSuggestedQuestions: 'Обновить предложения', inputPlaceholder: 'Введите ваше сообщение...', @@ -4998,6 +4999,24 @@ export default { capability: 'Расширения', integration: 'Публикация и интеграция', }, + questionSuggestions: { + navLabel: 'Рекомендуемые вопросы', title: 'Рекомендации вопросов в диалоге', + description: 'Единая политика агента для стартовых и контекстных вопросов после ответа.', + startersTitle: 'Стартовые вопросы', followUpsTitle: 'Вопросы после ответа', + enableStarters: 'Показывать стартовые вопросы', enableStartersDesc: 'Показываются до первого сообщения пользователя.', + enableFollowUps: 'Генерировать уточнения', enableFollowUpsDesc: 'Создаются асинхронно после каждого полного ответа и расходуют модель.', + sourceMode: 'Источник', count: 'Количество', curatedItems: 'Редакторские вопросы', + curatedItemsDesc: 'Имеют приоритет в гибридном режиме.', addItem: 'Добавить вопрос', + model: 'Модель генерации', modelDesc: 'Если не выбрана, используется модель текущего ответа.', + advancedSettings: 'Расширенные настройки генерации', + advancedSettingsDesc: 'Контекст, типы вопросов, инструкции и правила показа', + displayRules: 'Правила показа и резерва', + contextTurns: 'Ходы контекста', categories: 'Типы вопросов', instruction: 'Дополнительная инструкция', + suppressFallback: 'Скрывать после резервного ответа', suppressQuestion: 'Скрывать, если ответ заканчивается вопросом', + knowledgeFallback: 'Использовать базу знаний при ошибке', allowRegenerate: 'Разрешить обновление', + modeCurated: 'Редакторские', modeKnowledge: 'База знаний', modeGenerated: 'Модель', modeHybrid: 'Гибрид', + categoryClarify: 'Уточнение', categoryDeepen: 'Углубление', categoryAction: 'Следующий шаг', + }, placeholders: { available: 'Доступные переменные: ', clickToInsert: '(нажмите для вставки)', diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index 5c824dc89..35bba61be 100755 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -893,6 +893,7 @@ export default { title: "对话", newChat: "新对话", suggestedQuestions: "你可以这样问我", + followUpQuestions: "接下来可以继续问", suggestedQuestionsLoading: "正在加载推荐问题...", refreshSuggestedQuestions: "换一批推荐问题", inputPlaceholder: "请输入您的消息...", @@ -5504,6 +5505,41 @@ export default { capability: "能力扩展", integration: "发布集成", }, + questionSuggestions: { + navLabel: "问题推荐", + title: "对话问题推荐", + description: "统一配置开场问题与回答后的上下文追问;渠道可关闭展示,但不能改写智能体策略。", + startersTitle: "开场推荐", + followUpsTitle: "回答后推荐", + enableStarters: "展示开场问题", + enableStartersDesc: "在用户首次提问前展示,可使用运营配置、知识库或混合来源。", + enableFollowUps: "生成回答后推荐", + enableFollowUpsDesc: "每次完整回答结束后异步生成,不阻塞主回答。启用后会产生额外模型调用。", + sourceMode: "内容来源", + count: "展示数量", + curatedItems: "运营配置问题", + curatedItemsDesc: "用于开场推荐;混合模式下优先展示。", + addItem: "添加问题", + model: "生成模型", + modelDesc: "留空时使用本轮对话模型。", + advancedSettings: "高级生成设置", + advancedSettingsDesc: "上下文、问题类型、生成要求与展示规则", + displayRules: "展示与兜底规则", + contextTurns: "上下文轮数", + categories: "问题类型", + instruction: "附加生成要求", + suppressFallback: "兜底回答后不展示", + suppressQuestion: "回答本身以提问结尾时不展示", + knowledgeFallback: "模型生成失败时使用知识库候选", + allowRegenerate: "允许用户换一批", + modeCurated: "运营配置", + modeKnowledge: "知识库", + modeGenerated: "模型生成", + modeHybrid: "混合", + categoryClarify: "澄清", + categoryDeepen: "深入", + categoryAction: "下一步", + }, placeholders: { available: "可用变量:", clickToInsert: "(点击插入)", diff --git a/frontend/src/views/agent/AgentEditorModal.vue b/frontend/src/views/agent/AgentEditorModal.vue index bec0e240b..98e6d8f4a 100644 --- a/frontend/src/views/agent/AgentEditorModal.vue +++ b/frontend/src/views/agent/AgentEditorModal.vue @@ -825,6 +825,187 @@ + +
+
+

{{ $t('agentEditor.questionSuggestions.title') }}

+

{{ $t('agentEditor.questionSuggestions.description') }}

+
+ + + + + + +
+
+
+ +

{{ $t('agentEditor.questionSuggestions.enableStartersDesc') }}

+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ + + {{ formData.config.question_suggestions.starters.items.length }}/8 + +
+

{{ $t('agentEditor.questionSuggestions.curatedItemsDesc') }}

+
+
+
+
+ + + + +
+ + + {{ $t('agentEditor.questionSuggestions.addItem') }} + +
+
+
+
+ +
+
+
+ +

{{ $t('agentEditor.questionSuggestions.enableFollowUpsDesc') }}

+
+
+ +
+
+ + +
+
+
@@ -1487,6 +1668,7 @@ const copyAgentId = async () => { }; const currentSection = ref(props.initialSection || 'basic'); +const suggestionTab = ref<'starters' | 'followUps'>('starters'); const contentWrapperRef = ref(null); const highlightedField = ref(null); let highlightClearTimer: ReturnType | null = null; @@ -1922,6 +2104,7 @@ const navItems = computed(() => { { key: 'basic', icon: 'info-circle', label: t('agent.editor.basicInfo') }, { key: 'prompts', icon: 'file-paste', label: t('agent.editor.promptsConfig') }, { key: 'model', icon: 'control-platform', label: t('agent.editor.modelConfig') }, + { key: 'suggestions', icon: 'help-circle', label: t('agentEditor.questionSuggestions.navLabel') }, ]; // 多轮对话(仅普通模式显示,Agent模式内部自动控制) if (!isAgentMode.value) { @@ -1958,7 +2141,7 @@ const navGroups = computed(() => { { key: 'basic', label: t('agentEditor.navGroups.basic'), - items: pickItems(['basic', 'prompts', 'model', 'conversation']), + items: pickItems(['basic', 'prompts', 'model', 'conversation', 'suggestions']), }, { key: 'knowledge', @@ -2049,14 +2232,59 @@ const defaultFormData = { fallback_strategy: 'model' as 'fixed' | 'model', fallback_response: '', fallback_prompt: '', + question_suggestions: { + starters: { + enabled: true, + mode: 'hybrid' as 'curated' | 'knowledge' | 'hybrid', + items: [] as string[], + count: 6, + }, + follow_ups: { + enabled: false, + mode: 'hybrid' as 'generated' | 'knowledge' | 'hybrid', + count: 3, + model_id: '', + additional_instruction: '', + categories: ['clarify', 'deepen', 'action'] as Array<'clarify' | 'deepen' | 'action'>, + max_context_turns: 2, + suppress_on_fallback: true, + suppress_when_answer_asks_question: true, + knowledge_fallback: true, + allow_regenerate: false, + }, + }, // 已废弃字段(保留兼容) welcome_message: '', - suggested_prompts: [] as string[], } }; const formData = ref(JSON.parse(JSON.stringify(defaultFormData))); +const starterSuggestionModeOptions = computed(() => [ + { value: 'curated', label: t('agentEditor.questionSuggestions.modeCurated') }, + { value: 'knowledge', label: t('agentEditor.questionSuggestions.modeKnowledge') }, + { value: 'hybrid', label: t('agentEditor.questionSuggestions.modeHybrid') }, +]); +const followUpSuggestionModeOptions = computed(() => [ + { value: 'generated', label: t('agentEditor.questionSuggestions.modeGenerated') }, + { value: 'knowledge', label: t('agentEditor.questionSuggestions.modeKnowledge') }, + { value: 'hybrid', label: t('agentEditor.questionSuggestions.modeHybrid') }, +]); +const followUpCategoryOptions = computed(() => [ + { value: 'clarify', label: t('agentEditor.questionSuggestions.categoryClarify') }, + { value: 'deepen', label: t('agentEditor.questionSuggestions.categoryDeepen') }, + { value: 'action', label: t('agentEditor.questionSuggestions.categoryAction') }, +]); + +const addStarterSuggestion = () => { + const items = formData.value.config.question_suggestions.starters.items; + if (items.length < 8) items.push(''); +}; + +const removeStarterSuggestion = (index: number) => { + formData.value.config.question_suggestions.starters.items.splice(index, 1); +}; + const applyDefaultChatModelIfEmpty = () => { if (props.mode !== 'create' || !formData.value) return const chat = @@ -2570,8 +2798,20 @@ watch(() => props.visible, async (val) => { agentData.config.thinking = false; } + agentData.config.question_suggestions = { + starters: { + ...defaultFormData.config.question_suggestions.starters, + ...(agentData.config.question_suggestions?.starters || {}), + items: agentData.config.question_suggestions?.starters?.items || [], + }, + follow_ups: { + ...defaultFormData.config.question_suggestions.follow_ups, + ...(agentData.config.question_suggestions?.follow_ups || {}), + categories: agentData.config.question_suggestions?.follow_ups?.categories + || [...defaultFormData.config.question_suggestions.follow_ups.categories], + }, + }; // 确保数组字段存在 - if (!agentData.config.suggested_prompts) agentData.config.suggested_prompts = []; if (!agentData.config.knowledge_bases) agentData.config.knowledge_bases = []; if (!agentData.config.allowed_tools) agentData.config.allowed_tools = []; if (!agentData.config.mcp_services) agentData.config.mcp_services = []; @@ -3890,10 +4130,10 @@ const handleSave = async () => { // ReRank 模型按运行范围按需使用:知识库范围为 none,或未启用 // knowledge_search 时不需要;其余情况由对话入口在使用前给出明确提示。 - // 过滤空推荐问题 - if (formData.value.config.suggested_prompts) { - formData.value.config.suggested_prompts = formData.value.config.suggested_prompts.filter((p: string) => p.trim() !== ''); - } + formData.value.config.question_suggestions.starters.items = + formData.value.config.question_suggestions.starters.items + .map((p: string) => p.trim()) + .filter(Boolean); if (!formData.value.config.intent_prompts || Object.keys(formData.value.config.intent_prompts).length === 0) { delete formData.value.config.intent_prompts; @@ -4629,6 +4869,75 @@ const handleSave = async () => { } } +// 开场 / 回答后推荐用顶部 tab 区分(参照模型管理),避免整块包围框 +.suggestion-tabs { + margin-bottom: 4px; + + :deep(.t-tabs__nav-item) { + font-size: 14px; + } + + :deep(.t-tabs__operations) { + display: none; + } + + // 只用 tab 作导航,内容自行渲染在下方 + :deep(.t-tabs__content) { + display: none; + } +} + +.suggestion-advanced-divider { + display: flex; + align-items: center; + gap: 12px; + padding: 4px 0 2px; + color: var(--td-text-color-placeholder); + font-size: 12px; + line-height: 18px; + + &::before, + &::after { + content: ''; + flex: 1; + height: 1px; + background: var(--td-component-stroke); + } + + span { + flex-shrink: 0; + color: var(--td-text-color-secondary); + font-weight: 500; + } +} + +// 计数徽标紧贴标签,避免在整宽行里被 space-between 甩开 +// 需与基础 `.setting-info .setting-info-header`(space-between)同等特异性才能覆盖 +.setting-info-header.setting-info-header--inline { + justify-content: flex-start; + gap: 8px; +} + +.curated-items-count { + flex-shrink: 0; + padding: 0 8px; + height: 20px; + display: inline-flex; + align-items: center; + border-radius: 10px; + background: var(--td-bg-color-secondarycontainer); + font-size: 12px; + font-variant-numeric: tabular-nums; + color: var(--td-text-color-secondary); +} + +.suggestion-checkboxes { + display: flex; + flex-direction: column; + gap: 10px; + width: 100%; +} + // ===== 工具配置:overview 面板 ===== .tools-overview { display: flex; diff --git a/frontend/src/views/chat/index.vue b/frontend/src/views/chat/index.vue index a533292f5..c7c00660b 100644 --- a/frontend/src/views/chat/index.vue +++ b/frontend/src/views/chat/index.vue @@ -84,6 +84,14 @@ +
{ suggestedQuestionsFetchId++; @@ -309,6 +324,54 @@ const handleSuggestedQuestionClick = (question) => { } }; +const resolveAssistantMessageId = (message) => message?.id || message?.assistant_message_id; + +const loadFollowUpSuggestions = async (message, ensure = false, regenerate = false) => { + const messageId = resolveAssistantMessageId(message); + const targetSessionId = session_id.value; + if (!messageId || !targetSessionId || message.suggestionsDismissed) return; + message.suggestionLoading = true; + try { + let response = ensure + ? await ensureMessageSuggestions(targetSessionId, messageId, regenerate) + : await getMessageSuggestions(targetSessionId, messageId); + let set = response?.data; + for (let attempt = 0; set?.status === 'generating' && attempt < 120; attempt++) { + await new Promise((resolve) => setTimeout(resolve, 1000)); + if (session_id.value !== targetSessionId || message.suggestionsDismissed) return; + response = await getMessageSuggestions(targetSessionId, messageId); + set = response?.data; + } + message.suggestionSet = set?.status === 'ready' ? set : null; + } catch (error) { + if (ensure) console.warn('[FollowUpSuggestions] Failed to generate:', error); + message.suggestionSet = null; + } finally { + message.suggestionLoading = false; + nextTick(() => scrollToBottom()); + } +}; + +const recordSuggestionEvent = (message, set, eventType, questionId = '') => { + if (!set?.id) return; + void recordMessageSuggestionEvent(session_id.value, set.id, eventType, questionId).catch(() => undefined); +}; + +const handleFollowUpSelect = (message, item) => { + recordSuggestionEvent(message, message.suggestionSet, 'click', item.id); + pendingSuggestionAttribution = { + suggestion_set_id: message.suggestionSet.id, + question_id: item.id, + }; + if (inputFieldRef.value?.triggerSend) inputFieldRef.value.triggerSend(item.text); + else sendMsg(item.text); +}; + +const dismissSuggestions = (message, set) => { + message.suggestionsDismissed = true; + recordSuggestionEvent(message, set, 'dismiss'); +}; + // 防抖包装,切换知识库/文件时300ms内不重复请求 const debouncedFetchSuggestions = () => { if (historyLoading.value || messagesList.length > 0) return; @@ -466,6 +529,11 @@ const { scrollContainer, debug: import.meta.env.DEV, onAfterMsgList: async () => { + for (const message of messagesList) { + if (message.role === 'assistant' && message.is_completed && message.suggestionSet === undefined) { + void loadFollowUpSuggestions(message, false); + } + } const lastMessage = messagesList[messagesList.length - 1]; if (lastMessage && !lastMessage.is_completed) { isReplying.value = true; @@ -507,6 +575,9 @@ const { attachStreamDebugToMessage(message); pendingStreamDebug.value = null; }, + onTurnComplete: (message) => { + void loadFollowUpSuggestions(message, true); + }, }); const showGlobalTypingIndicator = computed(() => @@ -668,6 +739,8 @@ const sendMsg = async (value, modelId = '', mentionedItems = [], imageFiles = [] const requestMcpServiceIds = agentEnabled ? mcpServiceIds : []; const requestSkillNames = agentEnabled ? skillNames : []; + const suggestionAttribution = pendingSuggestionAttribution; + pendingSuggestionAttribution = null; await startStream({ session_id: session_id.value, knowledge_base_ids: kbIds, @@ -684,6 +757,7 @@ const sendMsg = async (value, modelId = '', mentionedItems = [], imageFiles = [] images: imageAttachments.length > 0 ? imageAttachments : undefined, attachment_uploads: attachmentUploads.length > 0 ? attachmentUploads : undefined, query: value, + suggestion_attribution: suggestionAttribution || undefined, method: 'POST', url: endpoint, }); diff --git a/frontend/src/views/embed/EmbedChatCore.vue b/frontend/src/views/embed/EmbedChatCore.vue index 71f55ebc5..7ac6087da 100644 --- a/frontend/src/views/embed/EmbedChatCore.vue +++ b/frontend/src/views/embed/EmbedChatCore.vue @@ -69,6 +69,14 @@ :embed-session-sig="sessionSig" :embed-visitor-id="visitorId" /> +
@@ -107,13 +115,22 @@