Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion cmd/wire_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions internal/base/constant/ai_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const (
- get_tags: 搜索标签信息
- get_tag_detail: 获取特定标签的详细信息
- get_user: 搜索用户信息
- semantic_search: 通过语义相似度搜索问题和答案。当用户的问题与现有内容概念相关但可能不匹配确切关键词时使用此工具。当 get_questions 关键词搜索返回较差结果时,请使用 semantic_search。

请根据用户的问题智能地使用这些工具来提供准确的答案。如果需要查询系统信息,请先使用相应的工具获取数据。`
DefaultAIPromptConfigEnUS = `You are an intelligent assistant that can help users query information in the system. User question: %s
Expand All @@ -44,6 +45,7 @@ You can use the following tools to query system information:
- get_tags: Search for tag information
- get_tag_detail: Get detailed information about a specific tag
- get_user: Search for user information
- semantic_search: Search questions and answers by semantic meaning. Use this when the user's question relates conceptually to existing content but may not match exact keywords. When get_questions keyword search returns poor results, use semantic_search instead.

Please intelligently use these tools based on the user's question to provide accurate answers. If you need to query system information, please use the appropriate tools to get the data first.`
)
7 changes: 7 additions & 0 deletions internal/controller/ai_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,7 @@ func (c *AIController) handleAIConversation(ctx *gin.Context, w http.ResponseWri
toolCalls, newMessages, finished, aiResponse := c.processAIStream(ctx, w, id, conversationCtx.Model, client, aiReq, messages)
messages = newMessages

log.Debugf("Round %d: toolCalls=%v", round+1, toolCalls)
if aiResponse != "" {
conversationCtx.Messages = append(conversationCtx.Messages, &ai_conversation.ConversationMessage{
Role: "assistant",
Expand Down Expand Up @@ -497,6 +498,10 @@ func (c *AIController) processAIStream(
break
}

if len(response.Choices) == 0 {
continue
}

choice := response.Choices[0]

if len(choice.Delta.ToolCalls) > 0 {
Expand Down Expand Up @@ -735,6 +740,8 @@ func (c *AIController) callMCPTool(ctx context.Context, toolName string, argumen
result, err = c.mcpController.MCPTagDetailsHandler()(ctx, request)
case "get_user":
result, err = c.mcpController.MCPUserDetailsHandler()(ctx, request)
case "semantic_search":
result, err = c.mcpController.MCPSemanticSearchHandler()(ctx, request)
default:
return "", fmt.Errorf("unknown tool: %s", toolName)
}
Expand Down
133 changes: 133 additions & 0 deletions internal/controller/mcp_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,13 @@ import (
answercommon "github.com/apache/answer/internal/service/answer_common"
"github.com/apache/answer/internal/service/comment"
"github.com/apache/answer/internal/service/content"
"github.com/apache/answer/internal/service/embedding"
"github.com/apache/answer/internal/service/feature_toggle"
questioncommon "github.com/apache/answer/internal/service/question_common"
"github.com/apache/answer/internal/service/siteinfo_common"
tagcommonser "github.com/apache/answer/internal/service/tag_common"
usercommon "github.com/apache/answer/internal/service/user_common"
"github.com/apache/answer/plugin"
"github.com/mark3labs/mcp-go/mcp"
"github.com/segmentfault/pacman/log"
)
Expand All @@ -49,6 +51,7 @@ type MCPController struct {
userCommon *usercommon.UserCommon
answerRepo answercommon.AnswerRepo
featureToggleSvc *feature_toggle.FeatureToggleService
embeddingService *embedding.EmbeddingService
}

// NewMCPController new site info controller.
Expand All @@ -61,6 +64,7 @@ func NewMCPController(
userCommon *usercommon.UserCommon,
answerRepo answercommon.AnswerRepo,
featureToggleSvc *feature_toggle.FeatureToggleService,
embeddingService *embedding.EmbeddingService,
) *MCPController {
return &MCPController{
searchService: searchService,
Expand All @@ -71,6 +75,7 @@ func NewMCPController(
userCommon: userCommon,
answerRepo: answerRepo,
featureToggleSvc: featureToggleSvc,
embeddingService: embeddingService,
}
}

Expand Down Expand Up @@ -349,3 +354,131 @@ func (c *MCPController) MCPUserDetailsHandler() func(ctx context.Context, reques
return mcp.NewToolResultText(string(res)), nil
}
}

func (c *MCPController) MCPSemanticSearchHandler() func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
if err := c.ensureMCPEnabled(ctx); err != nil {
return nil, err
}
cond := schema.NewMCPSemanticSearchCond(request)
if len(cond.Query) == 0 {
return mcp.NewToolResultText("Query is required for semantic search."), nil
}

siteGeneral, err := c.siteInfoService.GetSiteGeneral(ctx)
if err != nil {
log.Errorf("get site general info failed: %v", err)
return nil, err
}

results, err := c.embeddingService.SearchSimilar(ctx, cond.Query, cond.TopK)
if err != nil {
log.Errorf("semantic search failed: %v", err)
return mcp.NewToolResultText("Semantic search is not available. Embedding may not be configured."), nil
}
if len(results) == 0 {
return mcp.NewToolResultText("No semantically similar content found."), nil
}

resp := make([]*schema.MCPSemanticSearchResp, 0, len(results))
for _, r := range results {
var meta plugin.VectorSearchMetadata
_ = json.Unmarshal([]byte(r.Metadata), &meta)

item := &schema.MCPSemanticSearchResp{
ObjectID: r.ObjectID,
ObjectType: r.ObjectType,
Score: r.Score,
}

// Compose link from metadata
if r.ObjectType == "answer" && meta.AnswerID != "" {
item.Link = fmt.Sprintf("%s/questions/%s/%s", siteGeneral.SiteUrl, meta.QuestionID, meta.AnswerID)
} else {
item.Link = fmt.Sprintf("%s/questions/%s", siteGeneral.SiteUrl, meta.QuestionID)
}

// Query content from DB using IDs stored in metadata
if r.ObjectType == "question" {
question, qErr := c.questioncommon.Info(ctx, meta.QuestionID, "")
if qErr != nil {
log.Warnf("get question %s for semantic search failed: %v", meta.QuestionID, qErr)
} else {
item.Title = question.Title
item.Content = question.Content
}

// Fetch answers by ID from metadata
for _, a := range meta.Answers {
answerEntity, exist, aErr := c.answerRepo.GetAnswer(ctx, a.AnswerID)
if aErr != nil || !exist {
continue
}
answerItem := &schema.MCPSemanticSearchAnswer{
AnswerID: a.AnswerID,
Content: answerEntity.OriginalText,
}
// Fetch comments on this answer from DB
for _, ac := range a.Comments {
cmt, cExist, cErr := c.commentRepo.GetComment(ctx, ac.CommentID)
if cErr == nil && cExist {
answerItem.Comments = append(answerItem.Comments, &schema.MCPSemanticSearchComment{
CommentID: ac.CommentID,
Content: cmt.OriginalText,
})
}
}
item.Answers = append(item.Answers, answerItem)
}

// Fetch question comments from DB
for _, qc := range meta.Comments {
cmt, cExist, cErr := c.commentRepo.GetComment(ctx, qc.CommentID)
if cErr == nil && cExist {
item.Comments = append(item.Comments, &schema.MCPSemanticSearchComment{
CommentID: qc.CommentID,
Content: cmt.OriginalText,
})
}
}
} else if r.ObjectType == "answer" {
// Fetch question title for context
question, qErr := c.questioncommon.Info(ctx, meta.QuestionID, "")
if qErr == nil {
item.Title = question.Title
}

// Fetch answer content from DB
if meta.AnswerID != "" {
answerEntity, exist, aErr := c.answerRepo.GetAnswer(ctx, meta.AnswerID)
if aErr == nil && exist {
item.Content = answerEntity.OriginalText
}
} else if len(meta.Answers) > 0 {
answerEntity, exist, aErr := c.answerRepo.GetAnswer(ctx, meta.Answers[0].AnswerID)
if aErr == nil && exist {
item.Content = answerEntity.OriginalText
}
}

// Fetch answer comments from DB
if len(meta.Answers) > 0 {
for _, ac := range meta.Answers[0].Comments {
cmt, cExist, cErr := c.commentRepo.GetComment(ctx, ac.CommentID)
if cErr == nil && cExist {
item.Comments = append(item.Comments, &schema.MCPSemanticSearchComment{
CommentID: ac.CommentID,
Content: cmt.OriginalText,
})
}
}
}
}

resp = append(resp, item)
}

data, _ := json.Marshal(resp)
return mcp.NewToolResultText(string(data)), nil
}
}
Loading
Loading