Skip to content

Commit fa6157b

Browse files
authored
feat: enhance AI search functionality with response language support (#12460)
1 parent 30af267 commit fa6157b

File tree

9 files changed

+213
-113
lines changed

9 files changed

+213
-113
lines changed

agent/app/api/v2/file.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ func (b *BaseApi) FileAISearch(c *gin.Context) {
6363
if err := helper.CheckBindAndValidate(&req, c); err != nil {
6464
return
6565
}
66+
if strings.TrimSpace(req.ResponseLanguage) == "" {
67+
req.ResponseLanguage = strings.TrimSpace(c.GetHeader("Accept-Language"))
68+
}
6669
res, err := fileService.AISearch(req)
6770
if err != nil {
6871
helper.InternalServer(c, err)

agent/app/dto/request/file.go

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,19 @@ type FileOption struct {
1010
}
1111

1212
type FileAISearch struct {
13-
Path string `json:"path" validate:"required"`
14-
Query string `json:"query" validate:"required"`
15-
ContainSub *bool `json:"containSub,omitempty"`
16-
MaxItems int `json:"maxItems" validate:"omitempty,min=1,max=2000"`
17-
MatchCase bool `json:"matchCase"`
18-
WholeWord bool `json:"wholeWord"`
19-
UseRegex bool `json:"useRegex"`
20-
Extensions []string `json:"extensions,omitempty"`
21-
MinSize int64 `json:"minSize"`
22-
MaxSize int64 `json:"maxSize"`
23-
ModifiedAfter string `json:"modifiedAfter,omitempty"`
24-
ModifiedBefore string `json:"modifiedBefore,omitempty"`
13+
Path string `json:"path" validate:"required"`
14+
Query string `json:"query" validate:"required"`
15+
ResponseLanguage string `json:"responseLanguage,omitempty"`
16+
ContainSub *bool `json:"containSub,omitempty"`
17+
MaxItems int `json:"maxItems" validate:"omitempty,min=1,max=2000"`
18+
MatchCase bool `json:"matchCase"`
19+
WholeWord bool `json:"wholeWord"`
20+
UseRegex bool `json:"useRegex"`
21+
Extensions []string `json:"extensions,omitempty"`
22+
MinSize int64 `json:"minSize"`
23+
MaxSize int64 `json:"maxSize"`
24+
ModifiedAfter string `json:"modifiedAfter,omitempty"`
25+
ModifiedBefore string `json:"modifiedBefore,omitempty"`
2526

2627
MaxScanFiles int `json:"maxScanFiles"`
2728
MaxFileBytes int64 `json:"maxFileBytes"`

agent/app/service/file.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1167,9 +1167,15 @@ func (f *FileService) AISearch(req request.FileAISearch) (*response.FileAISearch
11671167
defer cancel()
11681168

11691169
llmMaxOut := searchOpts.LlmMaxOutputTokens
1170-
summary, usage, err := files.RunFileAISearchLLM(runCtx, cfg, clientTimeout, root, query, llmItems, truncated, preFiltered, contentHits, scannedFiles, hitsTrunc, matchDesc, searchOpts.ContentHitsPromptMaxBytes, llmMaxOut)
1170+
summary, usage, err := files.RunFileAISearchLLM(runCtx, cfg, clientTimeout, root, query, req.ResponseLanguage, llmItems, truncated, preFiltered, contentHits, scannedFiles, hitsTrunc, matchDesc, searchOpts.ContentHitsPromptMaxBytes, llmMaxOut)
11711171
if err != nil {
1172-
return nil, err
1172+
result.Mode = "grep"
1173+
result.Summary = ""
1174+
result.Duration = time.Since(start).Round(time.Millisecond).String()
1175+
if errors.Is(err, context.DeadlineExceeded) || strings.Contains(strings.ToLower(err.Error()), "timeout") {
1176+
return result, nil
1177+
}
1178+
return result, nil
11731179
}
11741180
result.Summary = summary
11751181
result.PromptTokens = usage.PromptTokens

agent/utils/files/ai_search_llm.go

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,33 @@ import (
1111

1212
const fileAISearchMaxPathRunes = 240
1313

14-
func buildFileAISearchSystemPrompt() string {
15-
return strings.Join([]string{
14+
func buildFileAISearchSystemPrompt(responseLanguage string) string {
15+
lines := []string{
1616
"You are a file browser assistant for a server panel.",
1717
"Answer using Markdown (headings, bullet lists).",
1818
"Only mention files and directories that appear in the provided inventory. Do not invent paths.",
1919
"If a \"Content line matches\" section is present, you may reference those lines and numbers only; never invent line numbers or snippets that are not listed there.",
2020
"If the inventory was truncated or incomplete, say so and suggest narrowing the directory or increasing limits.",
2121
"Group or rank results by relevance to the user's question when helpful.",
22-
"Prefer responding in the same language as the user's query (e.g. Chinese if the query is in Chinese).",
23-
}, "\n")
22+
}
23+
if lang := normalizeFileAISearchLanguage(responseLanguage); lang != "" {
24+
lines = append(lines, "Respond in "+lang+".")
25+
} else {
26+
lines = append(lines, "Prefer responding in the same language as the user's query (e.g. Chinese if the query is in Chinese).")
27+
}
28+
return strings.Join(lines, "\n")
2429
}
2530

26-
func buildFileAISearchUserPrompt(root, query string, items []AISearchInventoryItem, truncated, preFiltered bool, contentHits []FileAIContentHit, contentScannedFiles int, contentHitsTruncated bool, matchDesc string, promptHitMaxBytes int) string {
31+
func buildFileAISearchUserPrompt(root, query, responseLanguage string, items []AISearchInventoryItem, truncated, preFiltered bool, contentHits []FileAIContentHit, contentScannedFiles int, contentHitsTruncated bool, matchDesc string, promptHitMaxBytes int) string {
2732
var b strings.Builder
2833
b.WriteString("User question:\n")
2934
b.WriteString(strings.TrimSpace(query))
3035
b.WriteString("\n\nRoot directory:\n")
3136
b.WriteString(root)
37+
if lang := normalizeFileAISearchLanguage(responseLanguage); lang != "" {
38+
b.WriteString("\n\nPanel reply language:\n")
39+
b.WriteString(lang)
40+
}
3241
b.WriteString("\n\nInventory notes:\n")
3342
if truncated {
3443
b.WriteString("- Listing was truncated; not all files under the root were included.\n")
@@ -80,7 +89,7 @@ func buildFileAISearchUserPrompt(root, query string, items []AISearchInventoryIt
8089
return b.String()
8190
}
8291

83-
func RunFileAISearchLLM(ctx context.Context, cfg terminalai.GeneratorConfig, clientTimeout time.Duration, root, query string, items []AISearchInventoryItem, truncated, preFiltered bool, contentHits []FileAIContentHit, contentScannedFiles int, contentHitsTruncated bool, matchDesc string, promptHitMaxBytes, llmMaxOutputTokens int) (string, terminalai.ResponseUsage, error) {
92+
func RunFileAISearchLLM(ctx context.Context, cfg terminalai.GeneratorConfig, clientTimeout time.Duration, root, query, responseLanguage string, items []AISearchInventoryItem, truncated, preFiltered bool, contentHits []FileAIContentHit, contentScannedFiles int, contentHitsTruncated bool, matchDesc string, promptHitMaxBytes, llmMaxOutputTokens int) (string, terminalai.ResponseUsage, error) {
8493
timeout := clientTimeout
8594
if timeout <= 0 {
8695
timeout = 2 * time.Minute
@@ -109,8 +118,8 @@ func RunFileAISearchLLM(ctx context.Context, cfg terminalai.GeneratorConfig, cli
109118
}
110119
resp, err := client.ChatCompletion(ctx, terminalai.ChatCompletionRequest{
111120
Messages: []terminalai.ChatMessage{
112-
{Role: "system", Content: buildFileAISearchSystemPrompt()},
113-
{Role: "user", Content: buildFileAISearchUserPrompt(root, query, items, truncated, preFiltered, contentHits, contentScannedFiles, contentHitsTruncated, matchDesc, promptHitMaxBytes)},
121+
{Role: "system", Content: buildFileAISearchSystemPrompt(responseLanguage)},
122+
{Role: "user", Content: buildFileAISearchUserPrompt(root, query, responseLanguage, items, truncated, preFiltered, contentHits, contentScannedFiles, contentHitsTruncated, matchDesc, promptHitMaxBytes)},
114123
},
115124
MaxTokens: outTokens,
116125
})
@@ -123,3 +132,33 @@ func RunFileAISearchLLM(ctx context.Context, cfg terminalai.GeneratorConfig, cli
123132
}
124133
return summary, resp.Usage, nil
125134
}
135+
136+
func normalizeFileAISearchLanguage(lang string) string {
137+
lang = strings.TrimSpace(strings.ToLower(lang))
138+
switch {
139+
case lang == "", lang == "*":
140+
return "English"
141+
case strings.HasPrefix(lang, "zh-hant"), strings.HasPrefix(lang, "zh-tw"), strings.HasPrefix(lang, "zh-hk"):
142+
return "Traditional Chinese"
143+
case strings.HasPrefix(lang, "zh"):
144+
return "Simplified Chinese"
145+
case strings.HasPrefix(lang, "en"):
146+
return "English"
147+
case strings.HasPrefix(lang, "ja"):
148+
return "Japanese"
149+
case strings.HasPrefix(lang, "ko"):
150+
return "Korean"
151+
case strings.HasPrefix(lang, "ru"):
152+
return "Russian"
153+
case strings.HasPrefix(lang, "ms"):
154+
return "Malay"
155+
case strings.HasPrefix(lang, "tr"):
156+
return "Turkish"
157+
case strings.HasPrefix(lang, "pt-br"):
158+
return "Brazilian Portuguese"
159+
case strings.HasPrefix(lang, "es"):
160+
return "Spanish"
161+
default:
162+
return lang
163+
}
164+
}

frontend/src/api/interface/file.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export namespace File {
4242
export interface FileAISearchReq {
4343
path: string;
4444
query: string;
45+
responseLanguage?: string;
4546
containSub?: boolean;
4647
maxItems?: number;
4748
matchCase?: boolean;

frontend/src/api/modules/files.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export const getFilesList = (params: File.ReqFile) => {
1111
};
1212

1313
export const fileAiSearch = (params: File.FileAISearchReq) => {
14-
return http.post<File.FileAISearchResult>('files/ai-search', params, TimeoutEnum.T_5M);
14+
return http.post<File.FileAISearchResult>('files/ai-search', params, TimeoutEnum.T_10M);
1515
};
1616

1717
export const getFilesListByNode = (params: File.ReqNodeFile) => {

0 commit comments

Comments
 (0)