ds2api / internal /httpapi /openai /history /current_input_file.go
204848
Initial commit: ds2api with HF Spaces support
feccc69
Raw
History Blame Contribute Delete
9.79 kB
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)
}