mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-11 03:37:40 +08:00
247 lines
7.7 KiB
Go
247 lines
7.7 KiB
Go
package history
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"ds2api/internal/auth"
|
|
"ds2api/internal/config"
|
|
dsclient "ds2api/internal/deepseek/client"
|
|
"ds2api/internal/httpapi/openai/shared"
|
|
"ds2api/internal/promptcompat"
|
|
)
|
|
|
|
const (
|
|
currentInputFilename = promptcompat.CurrentInputContextFilename
|
|
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.DS == nil || s.Store == nil || a == nil || !s.Store.CurrentInputFileEnabled() {
|
|
return stdReq, nil
|
|
}
|
|
threshold := s.Store.CurrentInputFileMinChars()
|
|
|
|
index, text := latestUserInputForFile(stdReq.Messages)
|
|
if index < 0 {
|
|
return stdReq, nil
|
|
}
|
|
if len([]rune(text)) < threshold {
|
|
return stdReq, nil
|
|
}
|
|
fileText := promptcompat.BuildOpenAICurrentInputContextTranscript(stdReq.Messages)
|
|
if strings.TrimSpace(fileText) == "" {
|
|
return stdReq, errors.New("current user input file produced empty transcript")
|
|
}
|
|
toolsText, _ := promptcompat.BuildOpenAIToolsContextTranscript(stdReq.ToolsRaw, stdReq.ToolChoice)
|
|
modelType := "default"
|
|
if resolvedType, ok := config.GetModelType(stdReq.ResolvedModel); ok {
|
|
modelType = resolvedType
|
|
}
|
|
result, err := s.DS.UploadFile(ctx, a, dsclient.UploadFileRequest{
|
|
Filename: currentInputFilename,
|
|
ContentType: currentInputContentType,
|
|
Purpose: currentInputPurpose,
|
|
ModelType: modelType,
|
|
Data: []byte(fileText),
|
|
}, 3)
|
|
if err != nil {
|
|
return stdReq, fmt.Errorf("upload current user input file: %w", err)
|
|
}
|
|
fileID := strings.TrimSpace(result.ID)
|
|
if fileID == "" {
|
|
return stdReq, errors.New("upload current user input file returned empty file id")
|
|
}
|
|
|
|
toolFileID := ""
|
|
if strings.TrimSpace(toolsText) != "" {
|
|
result, 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, fmt.Errorf("upload current tools file: %w", err)
|
|
}
|
|
toolFileID = strings.TrimSpace(result.ID)
|
|
if toolFileID == "" {
|
|
return stdReq, errors.New("upload current tools file returned empty file id")
|
|
}
|
|
}
|
|
|
|
messages := []any{
|
|
map[string]any{
|
|
"role": "user",
|
|
"content": currentInputFilePrompt(toolFileID != ""),
|
|
},
|
|
}
|
|
|
|
stdReq.Messages = messages
|
|
stdReq.HistoryText = fileText
|
|
stdReq.CurrentInputFileApplied = true
|
|
stdReq.CurrentInputFileID = fileID
|
|
stdReq.CurrentToolsFileID = toolFileID
|
|
stdReq.RefFileIDs = prependUniqueRefFileIDs(stdReq.RefFileIDs, fileID, toolFileID)
|
|
stdReq.FinalPrompt, stdReq.ToolNames = promptcompat.BuildOpenAIPromptWithToolInstructionsOnly(messages, stdReq.ToolsRaw, "", stdReq.ToolChoice, stdReq.Thinking)
|
|
// Token accounting must reflect the actual downstream context:
|
|
// uploaded context files + the continuation live prompt.
|
|
tokenParts := []string{fileText}
|
|
if strings.TrimSpace(toolsText) != "" {
|
|
tokenParts = append(tokenParts, toolsText)
|
|
}
|
|
tokenParts = append(tokenParts, stdReq.FinalPrompt)
|
|
stdReq.PromptTokenText = strings.Join(tokenParts, "\n")
|
|
return stdReq, nil
|
|
}
|
|
|
|
func (s Service) ReuploadAppliedCurrentInputFile(ctx context.Context, a *auth.RequestAuth, stdReq promptcompat.StandardRequest) (promptcompat.StandardRequest, error) {
|
|
if !stdReq.CurrentInputFileApplied || s.DS == nil || a == nil {
|
|
return stdReq, nil
|
|
}
|
|
fileText := strings.TrimSpace(stdReq.HistoryText)
|
|
if fileText == "" {
|
|
return stdReq, nil
|
|
}
|
|
modelType := "default"
|
|
if resolvedType, ok := config.GetModelType(stdReq.ResolvedModel); ok {
|
|
modelType = resolvedType
|
|
}
|
|
result, err := s.DS.UploadFile(ctx, a, dsclient.UploadFileRequest{
|
|
Filename: currentInputFilename,
|
|
ContentType: currentInputContentType,
|
|
Purpose: currentInputPurpose,
|
|
ModelType: modelType,
|
|
Data: []byte(stdReq.HistoryText),
|
|
}, 3)
|
|
if err != nil {
|
|
return stdReq, fmt.Errorf("upload current user input file: %w", err)
|
|
}
|
|
fileID := strings.TrimSpace(result.ID)
|
|
if fileID == "" {
|
|
return stdReq, errors.New("upload current user input file returned empty file id")
|
|
}
|
|
|
|
toolsText, _ := promptcompat.BuildOpenAIToolsContextTranscript(stdReq.ToolsRaw, stdReq.ToolChoice)
|
|
toolFileID := ""
|
|
if strings.TrimSpace(toolsText) != "" {
|
|
result, 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, fmt.Errorf("upload current tools file: %w", err)
|
|
}
|
|
toolFileID = strings.TrimSpace(result.ID)
|
|
if toolFileID == "" {
|
|
return stdReq, errors.New("upload current tools file returned empty file id")
|
|
}
|
|
}
|
|
|
|
stdReq.RefFileIDs = replaceGeneratedCurrentInputRefs(stdReq.RefFileIDs, stdReq.CurrentInputFileID, stdReq.CurrentToolsFileID, fileID, toolFileID)
|
|
stdReq.CurrentInputFileID = fileID
|
|
stdReq.CurrentToolsFileID = toolFileID
|
|
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(hasToolsFile bool) string {
|
|
prompt := "Continue from the latest state in the attached DS2API_HISTORY.txt context. Treat it as the current working state and answer the latest user request directly."
|
|
if hasToolsFile {
|
|
prompt += " Available tool descriptions and parameter schemas are attached in DS2API_TOOLS.txt; use only those tools and follow the tool-call format rules in this prompt."
|
|
}
|
|
return prompt
|
|
}
|
|
|
|
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)
|
|
}
|