Skip to content

Commit 9792457

Browse files
committed
Merge remote-tracking branch 'origin/dev' into test
2 parents 2dbe594 + e3831b6 commit 9792457

18 files changed

Lines changed: 942 additions & 15 deletions

File tree

cmd/wire_gen.go

Lines changed: 9 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/base/constant/ai_config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const (
3333
- get_tags: 搜索标签信息
3434
- get_tag_detail: 获取特定标签的详细信息
3535
- get_user: 搜索用户信息
36+
- semantic_search: 通过语义相似度搜索问题和答案。当用户的问题与现有内容概念相关但可能不匹配确切关键词时使用此工具。当 get_questions 关键词搜索返回较差结果时,请使用 semantic_search。
3637
3738
请根据用户的问题智能地使用这些工具来提供准确的答案。如果需要查询系统信息,请先使用相应的工具获取数据。`
3839
DefaultAIPromptConfigEnUS = `You are an intelligent assistant that can help users query information in the system. User question: %s
@@ -44,6 +45,7 @@ You can use the following tools to query system information:
4445
- get_tags: Search for tag information
4546
- get_tag_detail: Get detailed information about a specific tag
4647
- get_user: Search for user information
48+
- 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.
4749
4850
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.`
4951
)

internal/cli/build.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,12 @@ func formatPlugins(plugins []string) (formatted []*pluginInfo) {
140140
info := &pluginInfo{}
141141
plugin, info.Path, _ = strings.Cut(plugin, "=")
142142
info.Name, info.Version, _ = strings.Cut(plugin, "@")
143+
// Resolve local path to absolute since build runs in a temp directory
144+
if len(info.Path) > 0 {
145+
if absPath, err := filepath.Abs(info.Path); err == nil {
146+
info.Path = absPath
147+
}
148+
}
143149
formatted = append(formatted, info)
144150
}
145151
return formatted
@@ -185,7 +191,12 @@ func createMainGoFile(b *buildingMaterial) (err error) {
185191
for _, p := range b.plugins {
186192
// If user set a path, use it to replace the module with local path
187193
if len(p.Path) > 0 {
188-
replacement := fmt.Sprintf("%s@%s=%s", p.Name, p.Version, p.Path)
194+
var replacement string
195+
if len(p.Version) > 0 {
196+
replacement = fmt.Sprintf("%s@%s=%s", p.Name, p.Version, p.Path)
197+
} else {
198+
replacement = fmt.Sprintf("%s=%s", p.Name, p.Path)
199+
}
189200
err = b.newExecCmd("go", "mod", "edit", "-replace", replacement).Run()
190201
} else if len(p.Version) > 0 {
191202
// If user specify a version, use it to get specific version of the module
@@ -415,6 +426,13 @@ func copyDirEntries(sourceFs fs.FS, sourceDir, targetDir string, ignoreDir ...st
415426
if strings.HasPrefix(path, s) {
416427
return true
417428
}
429+
// Also ignore nested occurrences, e.g. src/plugins/foo/node_modules
430+
if strings.Contains(path, string(filepath.Separator)+s) {
431+
return true
432+
}
433+
if strings.Contains(path, "/"+s) {
434+
return true
435+
}
418436
}
419437
return false
420438
}

internal/controller/ai_controller.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,7 @@ func (c *AIController) handleAIConversation(ctx *gin.Context, w http.ResponseWri
446446
toolCalls, newMessages, finished, aiResponse := c.processAIStream(ctx, w, id, conversationCtx.Model, client, aiReq, messages)
447447
messages = newMessages
448448

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

501+
if len(response.Choices) == 0 {
502+
continue
503+
}
504+
500505
choice := response.Choices[0]
501506

502507
if len(choice.Delta.ToolCalls) > 0 {
@@ -735,6 +740,8 @@ func (c *AIController) callMCPTool(ctx context.Context, toolName string, argumen
735740
result, err = c.mcpController.MCPTagDetailsHandler()(ctx, request)
736741
case "get_user":
737742
result, err = c.mcpController.MCPUserDetailsHandler()(ctx, request)
743+
case "semantic_search":
744+
result, err = c.mcpController.MCPSemanticSearchHandler()(ctx, request)
738745
default:
739746
return "", fmt.Errorf("unknown tool: %s", toolName)
740747
}

internal/controller/mcp_controller.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,13 @@ import (
3131
answercommon "github.com/apache/answer/internal/service/answer_common"
3232
"github.com/apache/answer/internal/service/comment"
3333
"github.com/apache/answer/internal/service/content"
34+
"github.com/apache/answer/internal/service/embedding"
3435
"github.com/apache/answer/internal/service/feature_toggle"
3536
questioncommon "github.com/apache/answer/internal/service/question_common"
3637
"github.com/apache/answer/internal/service/siteinfo_common"
3738
tagcommonser "github.com/apache/answer/internal/service/tag_common"
3839
usercommon "github.com/apache/answer/internal/service/user_common"
40+
"github.com/apache/answer/plugin"
3941
"github.com/mark3labs/mcp-go/mcp"
4042
"github.com/segmentfault/pacman/log"
4143
)
@@ -49,6 +51,7 @@ type MCPController struct {
4951
userCommon *usercommon.UserCommon
5052
answerRepo answercommon.AnswerRepo
5153
featureToggleSvc *feature_toggle.FeatureToggleService
54+
embeddingService *embedding.EmbeddingService
5255
}
5356

5457
// NewMCPController new site info controller.
@@ -61,6 +64,7 @@ func NewMCPController(
6164
userCommon *usercommon.UserCommon,
6265
answerRepo answercommon.AnswerRepo,
6366
featureToggleSvc *feature_toggle.FeatureToggleService,
67+
embeddingService *embedding.EmbeddingService,
6468
) *MCPController {
6569
return &MCPController{
6670
searchService: searchService,
@@ -71,6 +75,7 @@ func NewMCPController(
7175
userCommon: userCommon,
7276
answerRepo: answerRepo,
7377
featureToggleSvc: featureToggleSvc,
78+
embeddingService: embeddingService,
7479
}
7580
}
7681

@@ -349,3 +354,131 @@ func (c *MCPController) MCPUserDetailsHandler() func(ctx context.Context, reques
349354
return mcp.NewToolResultText(string(res)), nil
350355
}
351356
}
357+
358+
func (c *MCPController) MCPSemanticSearchHandler() func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
359+
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
360+
if err := c.ensureMCPEnabled(ctx); err != nil {
361+
return nil, err
362+
}
363+
cond := schema.NewMCPSemanticSearchCond(request)
364+
if len(cond.Query) == 0 {
365+
return mcp.NewToolResultText("Query is required for semantic search."), nil
366+
}
367+
368+
siteGeneral, err := c.siteInfoService.GetSiteGeneral(ctx)
369+
if err != nil {
370+
log.Errorf("get site general info failed: %v", err)
371+
return nil, err
372+
}
373+
374+
results, err := c.embeddingService.SearchSimilar(ctx, cond.Query, cond.TopK)
375+
if err != nil {
376+
log.Errorf("semantic search failed: %v", err)
377+
return mcp.NewToolResultText("Semantic search is not available. Embedding may not be configured."), nil
378+
}
379+
if len(results) == 0 {
380+
return mcp.NewToolResultText("No semantically similar content found."), nil
381+
}
382+
383+
resp := make([]*schema.MCPSemanticSearchResp, 0, len(results))
384+
for _, r := range results {
385+
var meta plugin.VectorSearchMetadata
386+
_ = json.Unmarshal([]byte(r.Metadata), &meta)
387+
388+
item := &schema.MCPSemanticSearchResp{
389+
ObjectID: r.ObjectID,
390+
ObjectType: r.ObjectType,
391+
Score: r.Score,
392+
}
393+
394+
// Compose link from metadata
395+
if r.ObjectType == "answer" && meta.AnswerID != "" {
396+
item.Link = fmt.Sprintf("%s/questions/%s/%s", siteGeneral.SiteUrl, meta.QuestionID, meta.AnswerID)
397+
} else {
398+
item.Link = fmt.Sprintf("%s/questions/%s", siteGeneral.SiteUrl, meta.QuestionID)
399+
}
400+
401+
// Query content from DB using IDs stored in metadata
402+
if r.ObjectType == "question" {
403+
question, qErr := c.questioncommon.Info(ctx, meta.QuestionID, "")
404+
if qErr != nil {
405+
log.Warnf("get question %s for semantic search failed: %v", meta.QuestionID, qErr)
406+
} else {
407+
item.Title = question.Title
408+
item.Content = question.Content
409+
}
410+
411+
// Fetch answers by ID from metadata
412+
for _, a := range meta.Answers {
413+
answerEntity, exist, aErr := c.answerRepo.GetAnswer(ctx, a.AnswerID)
414+
if aErr != nil || !exist {
415+
continue
416+
}
417+
answerItem := &schema.MCPSemanticSearchAnswer{
418+
AnswerID: a.AnswerID,
419+
Content: answerEntity.OriginalText,
420+
}
421+
// Fetch comments on this answer from DB
422+
for _, ac := range a.Comments {
423+
cmt, cExist, cErr := c.commentRepo.GetComment(ctx, ac.CommentID)
424+
if cErr == nil && cExist {
425+
answerItem.Comments = append(answerItem.Comments, &schema.MCPSemanticSearchComment{
426+
CommentID: ac.CommentID,
427+
Content: cmt.OriginalText,
428+
})
429+
}
430+
}
431+
item.Answers = append(item.Answers, answerItem)
432+
}
433+
434+
// Fetch question comments from DB
435+
for _, qc := range meta.Comments {
436+
cmt, cExist, cErr := c.commentRepo.GetComment(ctx, qc.CommentID)
437+
if cErr == nil && cExist {
438+
item.Comments = append(item.Comments, &schema.MCPSemanticSearchComment{
439+
CommentID: qc.CommentID,
440+
Content: cmt.OriginalText,
441+
})
442+
}
443+
}
444+
} else if r.ObjectType == "answer" {
445+
// Fetch question title for context
446+
question, qErr := c.questioncommon.Info(ctx, meta.QuestionID, "")
447+
if qErr == nil {
448+
item.Title = question.Title
449+
}
450+
451+
// Fetch answer content from DB
452+
if meta.AnswerID != "" {
453+
answerEntity, exist, aErr := c.answerRepo.GetAnswer(ctx, meta.AnswerID)
454+
if aErr == nil && exist {
455+
item.Content = answerEntity.OriginalText
456+
}
457+
} else if len(meta.Answers) > 0 {
458+
answerEntity, exist, aErr := c.answerRepo.GetAnswer(ctx, meta.Answers[0].AnswerID)
459+
if aErr == nil && exist {
460+
item.Content = answerEntity.OriginalText
461+
}
462+
}
463+
464+
// Fetch answer comments from DB
465+
if len(meta.Answers) > 0 {
466+
for _, ac := range meta.Answers[0].Comments {
467+
cmt, cExist, cErr := c.commentRepo.GetComment(ctx, ac.CommentID)
468+
if cErr == nil && cExist {
469+
item.Comments = append(item.Comments, &schema.MCPSemanticSearchComment{
470+
CommentID: ac.CommentID,
471+
Content: cmt.OriginalText,
472+
})
473+
}
474+
}
475+
}
476+
}
477+
478+
resp = append(resp, item)
479+
}
480+
481+
data, _ := json.Marshal(resp)
482+
return mcp.NewToolResultText(string(data)), nil
483+
}
484+
}

0 commit comments

Comments
 (0)