| package history |
|
|
| import ( |
| "context" |
| "crypto/rand" |
| "math/big" |
| "strings" |
| "time" |
| "unicode/utf8" |
|
|
| "ds2api/internal/auth" |
| "ds2api/internal/config" |
| dsclient "ds2api/internal/deepseek/client" |
| "ds2api/internal/httpapi/openai/shared" |
| "ds2api/internal/promptcompat" |
| ) |
|
|
| const ( |
| currentToolsFilename = promptcompat.CurrentToolsContextFilename |
| currentInputContentType = "text/plain; charset=utf-8" |
| currentInputPurpose = "assistants" |
| ) |
|
|
| type CurrentInputConfigReader interface { |
| CurrentInputFileEnabled() bool |
| CurrentInputFileMinChars() int |
| } |
|
|
| type CurrentInputUploader interface { |
| UploadFile(ctx context.Context, a *auth.RequestAuth, req dsclient.UploadFileRequest, maxAttempts int) (*dsclient.UploadFileResult, error) |
| } |
|
|
| type Service struct { |
| Store CurrentInputConfigReader |
| DS CurrentInputUploader |
| } |
|
|
| func (s Service) ApplyCurrentInputFile(ctx context.Context, a *auth.RequestAuth, stdReq promptcompat.StandardRequest) (promptcompat.StandardRequest, error) { |
| if stdReq.CurrentInputFileApplied || s.Store == nil || s.DS == nil || !s.Store.CurrentInputFileEnabled() { |
| return stdReq, nil |
| } |
| modelID := strings.TrimSpace(stdReq.ResolvedModel) |
| if modelID == "" { |
| modelID = strings.TrimSpace(stdReq.RequestedModel) |
| } |
| modelType, ok := config.GetModelType(modelID) |
| if !ok || modelType != "default" { |
| return stdReq, nil |
| } |
| _, latestText := latestUserInputForFile(stdReq.Messages) |
| if strings.TrimSpace(latestText) == "" { |
| return stdReq, nil |
| } |
| minChars := s.Store.CurrentInputFileMinChars() |
| if minChars > 0 && utf8.RuneCountInString(latestText) < minChars { |
| return stdReq, nil |
| } |
| historyText := promptcompat.BuildOpenAICurrentInputContextTranscript(stdReq.Messages) |
| if strings.TrimSpace(historyText) == "" { |
| return stdReq, nil |
| } |
| filename := strings.TrimSpace(stdReq.CurrentInputFilename) |
| if filename == "" { |
| filename = randomContextFilename() |
| } |
| toolsText, toolNames := promptcompat.BuildOpenAIToolsContextTranscript(stdReq.ToolsRaw, stdReq.ToolChoice) |
| hasToolsFile := strings.TrimSpace(toolsText) != "" |
|
|
| historyResult, err := s.DS.UploadFile(ctx, a, dsclient.UploadFileRequest{ |
| Filename: filename, |
| ContentType: currentInputContentType, |
| Purpose: currentInputPurpose, |
| ModelType: modelType, |
| Data: []byte(historyText), |
| }, 3) |
| if err != nil { |
| return stdReq, err |
| } |
| historyID := "" |
| if historyResult != nil { |
| historyID = strings.TrimSpace(historyResult.ID) |
| } |
|
|
| toolsID := "" |
| if hasToolsFile { |
| toolsResult, err := s.DS.UploadFile(ctx, a, dsclient.UploadFileRequest{ |
| Filename: currentToolsFilename, |
| ContentType: currentInputContentType, |
| Purpose: currentInputPurpose, |
| ModelType: modelType, |
| Data: []byte(toolsText), |
| }, 3) |
| if err != nil { |
| return stdReq, err |
| } |
| if toolsResult != nil { |
| toolsID = strings.TrimSpace(toolsResult.ID) |
| } |
| } |
|
|
| promptMessages := make([]any, 0, 2) |
| if hasToolsFile { |
| toolInstructions, instructionNames := promptcompat.BuildOpenAIToolPromptInstructions(stdReq.ToolsRaw, stdReq.ToolChoice) |
| if len(stdReq.ToolNames) == 0 { |
| if len(instructionNames) > 0 { |
| stdReq.ToolNames = instructionNames |
| } else if len(toolNames) > 0 { |
| stdReq.ToolNames = toolNames |
| } |
| } |
| systemText := "Tool descriptions and parameter schemas are attached in context_tools.txt. Use only those tools." |
| if strings.TrimSpace(toolInstructions) != "" { |
| systemText += "\n\n" + toolInstructions |
| } |
| promptMessages = append(promptMessages, map[string]any{ |
| "role": "system", |
| "content": systemText, |
| }) |
| } else if len(stdReq.ToolNames) == 0 && len(toolNames) > 0 { |
| stdReq.ToolNames = toolNames |
| } |
| promptMessages = append(promptMessages, map[string]any{ |
| "role": "user", |
| "content": currentInputFilePrompt(filename, hasToolsFile), |
| }) |
| finalPrompt, _ := promptcompat.BuildOpenAIPrompt(promptMessages, nil, "", stdReq.ToolChoice, stdReq.Thinking) |
| if strings.TrimSpace(finalPrompt) == "" { |
| finalPrompt = currentInputFilePrompt(filename, hasToolsFile) |
| } |
|
|
| promptTokenText := historyText |
| if strings.TrimSpace(toolsText) != "" { |
| if !strings.HasSuffix(promptTokenText, "\n") { |
| promptTokenText += "\n" |
| } |
| promptTokenText += toolsText |
| } |
|
|
| stdReq.CurrentInputFileApplied = true |
| stdReq.CurrentInputFileID = historyID |
| stdReq.CurrentInputFilename = filename |
| stdReq.CurrentToolsFileID = toolsID |
| stdReq.HistoryText = historyText |
| stdReq.Messages = promptMessages |
| stdReq.FinalPrompt = finalPrompt |
| stdReq.PromptTokenText = promptTokenText |
| stdReq.RefFileIDs = prependUniqueRefFileIDs(stdReq.RefFileIDs, historyID, toolsID) |
| return stdReq, nil |
| } |
|
|
| func (s Service) ReuploadAppliedCurrentInputFile(ctx context.Context, a *auth.RequestAuth, stdReq promptcompat.StandardRequest) (promptcompat.StandardRequest, error) { |
| if !stdReq.CurrentInputFileApplied || s.Store == nil || s.DS == nil { |
| return stdReq, nil |
| } |
| modelID := strings.TrimSpace(stdReq.ResolvedModel) |
| if modelID == "" { |
| modelID = strings.TrimSpace(stdReq.RequestedModel) |
| } |
| modelType, ok := config.GetModelType(modelID) |
| if !ok || modelType != "default" { |
| return stdReq, nil |
| } |
| historyText := stdReq.HistoryText |
| if strings.TrimSpace(historyText) == "" { |
| return stdReq, nil |
| } |
| filename := strings.TrimSpace(stdReq.CurrentInputFilename) |
| if filename == "" { |
| filename = randomContextFilename() |
| } |
| toolsText, _ := promptcompat.BuildOpenAIToolsContextTranscript(stdReq.ToolsRaw, stdReq.ToolChoice) |
| hasToolsFile := strings.TrimSpace(toolsText) != "" |
|
|
| newHistory, err := s.DS.UploadFile(ctx, a, dsclient.UploadFileRequest{ |
| Filename: filename, |
| ContentType: currentInputContentType, |
| Purpose: currentInputPurpose, |
| ModelType: modelType, |
| Data: []byte(historyText), |
| }, 3) |
| if err != nil { |
| return stdReq, err |
| } |
| newHistoryID := "" |
| if newHistory != nil { |
| newHistoryID = strings.TrimSpace(newHistory.ID) |
| } |
|
|
| newToolsID := "" |
| if hasToolsFile { |
| newTools, err := s.DS.UploadFile(ctx, a, dsclient.UploadFileRequest{ |
| Filename: currentToolsFilename, |
| ContentType: currentInputContentType, |
| Purpose: currentInputPurpose, |
| ModelType: modelType, |
| Data: []byte(toolsText), |
| }, 3) |
| if err != nil { |
| return stdReq, err |
| } |
| if newTools != nil { |
| newToolsID = strings.TrimSpace(newTools.ID) |
| } |
| } |
|
|
| stdReq.RefFileIDs = replaceGeneratedCurrentInputRefs(stdReq.RefFileIDs, stdReq.CurrentInputFileID, stdReq.CurrentToolsFileID, newHistoryID, newToolsID) |
| stdReq.CurrentInputFileID = newHistoryID |
| stdReq.CurrentInputFilename = filename |
| stdReq.CurrentToolsFileID = newToolsID |
| return stdReq, nil |
| } |
|
|
| func latestUserInputForFile(messages []any) (int, string) { |
| for i := len(messages) - 1; i >= 0; i-- { |
| msg, ok := messages[i].(map[string]any) |
| if !ok { |
| continue |
| } |
| role := strings.ToLower(strings.TrimSpace(shared.AsString(msg["role"]))) |
| if role != "user" { |
| continue |
| } |
| text := promptcompat.NormalizeOpenAIContentForPrompt(msg["content"]) |
| if strings.TrimSpace(text) == "" { |
| return -1, "" |
| } |
| return i, text |
| } |
| return -1, "" |
| } |
|
|
| func currentInputFilePrompt(filename string, hasToolsFile bool) string { |
| base := []string{ |
| "已附上下文文件「%s」。请直接回答最新问题,不要提及文件、上下文或提示词本身。", |
| "请从文件 %s 继续处理本轮任务,给出答案,避免说明你在使用上下文文件。", |
| "上下文已存入 %s,请将其视为唯一上下文并直接答复;不要复述提示或提到文件。", |
| } |
| prompt := pickPromptVariant(base) |
| text := strings.ReplaceAll(prompt, "%s", filename) |
| if hasToolsFile { |
| text += " 可用工具与参数说明在 context_tools.txt 中,仅可依据该文件调用工具。" |
| } |
| return text |
| } |
|
|
| func pickPromptVariant(variants []string) string { |
| if len(variants) == 0 { |
| return "" |
| } |
| idx := 0 |
| if len(variants) > 1 { |
| max := big.NewInt(int64(len(variants))) |
| if v, err := rand.Int(rand.Reader, max); err == nil { |
| idx = int(v.Int64()) |
| } else { |
| idx = int(time.Now().UnixNano()) % len(variants) |
| if idx < 0 { |
| idx = 0 |
| } |
| } |
| } |
| return variants[idx] |
| } |
|
|
| func randomContextFilename() string { |
| const letters = "abcdefghijklmnopqrstuvwxyz0123456789" |
| length := 12 |
| max := big.NewInt(int64(len(letters))) |
| b := make([]byte, 0, length) |
| for i := 0; i < length; i++ { |
| idx, err := rand.Int(rand.Reader, max) |
| if err != nil { |
| idx = big.NewInt(int64(time.Now().UnixNano() % int64(len(letters)))) |
| } |
| b = append(b, letters[idx.Int64()]) |
| } |
| return "ctx-" + string(b) + ".txt" |
| } |
|
|
| func prependUniqueRefFileIDs(existing []string, fileIDs ...string) []string { |
| out := make([]string, 0, len(existing)+len(fileIDs)) |
| seen := map[string]struct{}{} |
| for _, fileID := range fileIDs { |
| trimmed := strings.TrimSpace(fileID) |
| if trimmed == "" { |
| continue |
| } |
| key := strings.ToLower(trimmed) |
| if _, ok := seen[key]; ok { |
| continue |
| } |
| out = append(out, trimmed) |
| seen[key] = struct{}{} |
| } |
| for _, id := range existing { |
| trimmed := strings.TrimSpace(id) |
| if trimmed == "" { |
| continue |
| } |
| key := strings.ToLower(trimmed) |
| if _, ok := seen[key]; ok { |
| continue |
| } |
| out = append(out, trimmed) |
| seen[key] = struct{}{} |
| } |
| return out |
| } |
|
|
| func replaceGeneratedCurrentInputRefs(existing []string, oldHistoryID, oldToolsID, newHistoryID, newToolsID string) []string { |
| filtered := make([]string, 0, len(existing)) |
| old := map[string]struct{}{} |
| for _, id := range []string{oldHistoryID, oldToolsID} { |
| trimmed := strings.ToLower(strings.TrimSpace(id)) |
| if trimmed != "" { |
| old[trimmed] = struct{}{} |
| } |
| } |
| for _, id := range existing { |
| trimmed := strings.TrimSpace(id) |
| if trimmed == "" { |
| continue |
| } |
| if _, ok := old[strings.ToLower(trimmed)]; ok { |
| continue |
| } |
| filtered = append(filtered, trimmed) |
| } |
| return prependUniqueRefFileIDs(filtered, newHistoryID, newToolsID) |
| } |
|
|