Skip to content
Merged
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
3 changes: 3 additions & 0 deletions agent/app/api/v2/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ func (b *BaseApi) FileAISearch(c *gin.Context) {
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if strings.TrimSpace(req.ResponseLanguage) == "" {
req.ResponseLanguage = strings.TrimSpace(c.GetHeader("Accept-Language"))
}
res, err := fileService.AISearch(req)
if err != nil {
helper.InternalServer(c, err)
Expand Down
25 changes: 13 additions & 12 deletions agent/app/dto/request/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,19 @@ type FileOption struct {
}

type FileAISearch struct {
Path string `json:"path" validate:"required"`
Query string `json:"query" validate:"required"`
ContainSub *bool `json:"containSub,omitempty"`
MaxItems int `json:"maxItems" validate:"omitempty,min=1,max=2000"`
MatchCase bool `json:"matchCase"`
WholeWord bool `json:"wholeWord"`
UseRegex bool `json:"useRegex"`
Extensions []string `json:"extensions,omitempty"`
MinSize int64 `json:"minSize"`
MaxSize int64 `json:"maxSize"`
ModifiedAfter string `json:"modifiedAfter,omitempty"`
ModifiedBefore string `json:"modifiedBefore,omitempty"`
Path string `json:"path" validate:"required"`
Query string `json:"query" validate:"required"`
ResponseLanguage string `json:"responseLanguage,omitempty"`
ContainSub *bool `json:"containSub,omitempty"`
MaxItems int `json:"maxItems" validate:"omitempty,min=1,max=2000"`
MatchCase bool `json:"matchCase"`
WholeWord bool `json:"wholeWord"`
UseRegex bool `json:"useRegex"`
Extensions []string `json:"extensions,omitempty"`
MinSize int64 `json:"minSize"`
MaxSize int64 `json:"maxSize"`
ModifiedAfter string `json:"modifiedAfter,omitempty"`
ModifiedBefore string `json:"modifiedBefore,omitempty"`

MaxScanFiles int `json:"maxScanFiles"`
MaxFileBytes int64 `json:"maxFileBytes"`
Expand Down
10 changes: 8 additions & 2 deletions agent/app/service/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -1167,9 +1167,15 @@ func (f *FileService) AISearch(req request.FileAISearch) (*response.FileAISearch
defer cancel()

llmMaxOut := searchOpts.LlmMaxOutputTokens
summary, usage, err := files.RunFileAISearchLLM(runCtx, cfg, clientTimeout, root, query, llmItems, truncated, preFiltered, contentHits, scannedFiles, hitsTrunc, matchDesc, searchOpts.ContentHitsPromptMaxBytes, llmMaxOut)
summary, usage, err := files.RunFileAISearchLLM(runCtx, cfg, clientTimeout, root, query, req.ResponseLanguage, llmItems, truncated, preFiltered, contentHits, scannedFiles, hitsTrunc, matchDesc, searchOpts.ContentHitsPromptMaxBytes, llmMaxOut)
if err != nil {
return nil, err
result.Mode = "grep"
result.Summary = ""
result.Duration = time.Since(start).Round(time.Millisecond).String()
if errors.Is(err, context.DeadlineExceeded) || strings.Contains(strings.ToLower(err.Error()), "timeout") {
return result, nil
}
return result, nil
}
result.Summary = summary
result.PromptTokens = usage.PromptTokens
Expand Down
55 changes: 47 additions & 8 deletions agent/utils/files/ai_search_llm.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,33 @@ import (

const fileAISearchMaxPathRunes = 240

func buildFileAISearchSystemPrompt() string {
return strings.Join([]string{
func buildFileAISearchSystemPrompt(responseLanguage string) string {
lines := []string{
"You are a file browser assistant for a server panel.",
"Answer using Markdown (headings, bullet lists).",
"Only mention files and directories that appear in the provided inventory. Do not invent paths.",
"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.",
"If the inventory was truncated or incomplete, say so and suggest narrowing the directory or increasing limits.",
"Group or rank results by relevance to the user's question when helpful.",
"Prefer responding in the same language as the user's query (e.g. Chinese if the query is in Chinese).",
}, "\n")
}
if lang := normalizeFileAISearchLanguage(responseLanguage); lang != "" {
lines = append(lines, "Respond in "+lang+".")
} else {
lines = append(lines, "Prefer responding in the same language as the user's query (e.g. Chinese if the query is in Chinese).")
}
return strings.Join(lines, "\n")
}

func buildFileAISearchUserPrompt(root, query string, items []AISearchInventoryItem, truncated, preFiltered bool, contentHits []FileAIContentHit, contentScannedFiles int, contentHitsTruncated bool, matchDesc string, promptHitMaxBytes int) string {
func buildFileAISearchUserPrompt(root, query, responseLanguage string, items []AISearchInventoryItem, truncated, preFiltered bool, contentHits []FileAIContentHit, contentScannedFiles int, contentHitsTruncated bool, matchDesc string, promptHitMaxBytes int) string {
var b strings.Builder
b.WriteString("User question:\n")
b.WriteString(strings.TrimSpace(query))
b.WriteString("\n\nRoot directory:\n")
b.WriteString(root)
if lang := normalizeFileAISearchLanguage(responseLanguage); lang != "" {
b.WriteString("\n\nPanel reply language:\n")
b.WriteString(lang)
}
b.WriteString("\n\nInventory notes:\n")
if truncated {
b.WriteString("- Listing was truncated; not all files under the root were included.\n")
Expand Down Expand Up @@ -80,7 +89,7 @@ func buildFileAISearchUserPrompt(root, query string, items []AISearchInventoryIt
return b.String()
}

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) {
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) {
timeout := clientTimeout
if timeout <= 0 {
timeout = 2 * time.Minute
Expand Down Expand Up @@ -109,8 +118,8 @@ func RunFileAISearchLLM(ctx context.Context, cfg terminalai.GeneratorConfig, cli
}
resp, err := client.ChatCompletion(ctx, terminalai.ChatCompletionRequest{
Messages: []terminalai.ChatMessage{
{Role: "system", Content: buildFileAISearchSystemPrompt()},
{Role: "user", Content: buildFileAISearchUserPrompt(root, query, items, truncated, preFiltered, contentHits, contentScannedFiles, contentHitsTruncated, matchDesc, promptHitMaxBytes)},
{Role: "system", Content: buildFileAISearchSystemPrompt(responseLanguage)},
{Role: "user", Content: buildFileAISearchUserPrompt(root, query, responseLanguage, items, truncated, preFiltered, contentHits, contentScannedFiles, contentHitsTruncated, matchDesc, promptHitMaxBytes)},
},
MaxTokens: outTokens,
})
Expand All @@ -123,3 +132,33 @@ func RunFileAISearchLLM(ctx context.Context, cfg terminalai.GeneratorConfig, cli
}
return summary, resp.Usage, nil
}

func normalizeFileAISearchLanguage(lang string) string {
lang = strings.TrimSpace(strings.ToLower(lang))
switch {
case lang == "", lang == "*":
return "English"
case strings.HasPrefix(lang, "zh-hant"), strings.HasPrefix(lang, "zh-tw"), strings.HasPrefix(lang, "zh-hk"):
return "Traditional Chinese"
case strings.HasPrefix(lang, "zh"):
return "Simplified Chinese"
case strings.HasPrefix(lang, "en"):
return "English"
case strings.HasPrefix(lang, "ja"):
return "Japanese"
case strings.HasPrefix(lang, "ko"):
return "Korean"
case strings.HasPrefix(lang, "ru"):
return "Russian"
case strings.HasPrefix(lang, "ms"):
return "Malay"
case strings.HasPrefix(lang, "tr"):
return "Turkish"
case strings.HasPrefix(lang, "pt-br"):
return "Brazilian Portuguese"
case strings.HasPrefix(lang, "es"):
return "Spanish"
default:
return lang
}
}
1 change: 1 addition & 0 deletions frontend/src/api/interface/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export namespace File {
export interface FileAISearchReq {
path: string;
query: string;
responseLanguage?: string;
containSub?: boolean;
maxItems?: number;
matchCase?: boolean;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/api/modules/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const getFilesList = (params: File.ReqFile) => {
};

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

export const getFilesListByNode = (params: File.ReqNodeFile) => {
Expand Down
Loading
Loading