feat: support file-aware model switch and update tech docs
This commit is contained in:
@@ -46,9 +46,11 @@ type FeishuConfig struct {
|
||||
}
|
||||
|
||||
type LLMConfig struct {
|
||||
BaseURL string
|
||||
APIKey string
|
||||
Model string
|
||||
BaseURL string
|
||||
APIKey string
|
||||
Model string
|
||||
FileModel string
|
||||
FilePromptMode string
|
||||
}
|
||||
|
||||
type SecurityConfig struct {
|
||||
@@ -94,9 +96,11 @@ func Load() (Config, error) {
|
||||
EventPath: defaultIfEmpty(os.Getenv("FEISHU_EVENT_PATH"), "/feishu/events"),
|
||||
},
|
||||
LLM: LLMConfig{
|
||||
BaseURL: strings.TrimRight(defaultIfEmpty(os.Getenv("LLM_BASE_URL"), "https://api.openai.com/v1"), "/"),
|
||||
APIKey: strings.TrimSpace(os.Getenv("LLM_API_KEY")),
|
||||
Model: defaultIfEmpty(os.Getenv("LLM_MODEL"), "gpt-4o-mini"),
|
||||
BaseURL: strings.TrimRight(defaultIfEmpty(os.Getenv("LLM_BASE_URL"), "https://api.openai.com/v1"), "/"),
|
||||
APIKey: strings.TrimSpace(os.Getenv("LLM_API_KEY")),
|
||||
Model: defaultIfEmpty(os.Getenv("LLM_MODEL"), "gpt-4o-mini"),
|
||||
FileModel: defaultIfEmpty(os.Getenv("LLM_FILE_MODEL"), defaultIfEmpty(os.Getenv("LLM_MODEL"), "gpt-4o-mini")),
|
||||
FilePromptMode: normalizeFilePromptMode(defaultIfEmpty(os.Getenv("LLM_FILE_PROMPT_MODE"), "user_content_file_parts")),
|
||||
},
|
||||
SQLitePath: defaultIfEmpty(os.Getenv("SQLITE_PATH"), filepath.Join(defaultDataDir, "laodingbot.db")),
|
||||
WebSearch: WebSearchConfig{
|
||||
@@ -155,6 +159,9 @@ func Load() (Config, error) {
|
||||
if cfg.LLM.APIKey == "" {
|
||||
return Config{}, fmt.Errorf("LLM_API_KEY is required")
|
||||
}
|
||||
if cfg.LLM.FilePromptMode != "user_content_file_parts" && cfg.LLM.FilePromptMode != "system_fileid_uri" {
|
||||
return Config{}, fmt.Errorf("LLM_FILE_PROMPT_MODE must be one of: user_content_file_parts, system_fileid_uri")
|
||||
}
|
||||
|
||||
cfg.SoulPath = resolvePathInWorkspace(cfg.SoulPath, agentWorkspaceDir)
|
||||
cfg.SkillsDir = resolvePathInWorkspace(cfg.SkillsDir, agentWorkspaceDir)
|
||||
@@ -391,3 +398,14 @@ func splitCSV(raw string) []string {
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func normalizeFilePromptMode(v string) string {
|
||||
v = strings.ToLower(strings.TrimSpace(v))
|
||||
if v == "" {
|
||||
return "user_content_file_parts"
|
||||
}
|
||||
if v == "system_fileid" || v == "system_fileid_url" || v == "system_fileid_uri" {
|
||||
return "system_fileid_uri"
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
@@ -34,20 +34,24 @@ type InputFile struct {
|
||||
}
|
||||
|
||||
type OpenAICompatibleClient struct {
|
||||
baseURL string
|
||||
apiKey string
|
||||
model string
|
||||
http *http.Client
|
||||
log *logger.Logger
|
||||
baseURL string
|
||||
apiKey string
|
||||
model string
|
||||
fileModel string
|
||||
filePromptMode string
|
||||
http *http.Client
|
||||
log *logger.Logger
|
||||
}
|
||||
|
||||
func NewOpenAICompatibleClient(cfg config.LLMConfig, log *logger.Logger) *OpenAICompatibleClient {
|
||||
return &OpenAICompatibleClient{
|
||||
baseURL: cfg.BaseURL,
|
||||
apiKey: cfg.APIKey,
|
||||
model: cfg.Model,
|
||||
http: &http.Client{Timeout: 60 * time.Second},
|
||||
log: log,
|
||||
baseURL: cfg.BaseURL,
|
||||
apiKey: cfg.APIKey,
|
||||
model: cfg.Model,
|
||||
fileModel: cfg.FileModel,
|
||||
filePromptMode: cfg.FilePromptMode,
|
||||
http: &http.Client{Timeout: 60 * time.Second},
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,16 +111,20 @@ func (c *OpenAICompatibleClient) GenerateWithFiles(ctx context.Context, systemPr
|
||||
}
|
||||
|
||||
func (c *OpenAICompatibleClient) generateInternal(ctx context.Context, systemPrompt, userPrompt string, fileIDs []string) (string, error) {
|
||||
if c.log != nil {
|
||||
c.log.Debugf("llm request start model=%s system_len=%d user_len=%d file_count=%d", c.model, len(systemPrompt), len(userPrompt), len(fileIDs))
|
||||
model := c.model
|
||||
ids := nonEmptyIDs(fileIDs)
|
||||
if len(ids) > 0 {
|
||||
if strings.TrimSpace(c.fileModel) != "" {
|
||||
model = c.fileModel
|
||||
}
|
||||
}
|
||||
userContent := buildUserContent(userPrompt, fileIDs)
|
||||
if c.log != nil {
|
||||
c.log.Debugf("llm request start model=%s system_len=%d user_len=%d file_count=%d file_prompt_mode=%s", model, len(systemPrompt), len(userPrompt), len(ids), c.normalizedFilePromptMode())
|
||||
}
|
||||
messages := buildMessages(systemPrompt, userPrompt, ids, c.normalizedFilePromptMode())
|
||||
body := chatRequest{
|
||||
Model: c.model,
|
||||
Messages: []chatMessage{
|
||||
{Role: "system", Content: systemPrompt},
|
||||
{Role: "user", Content: userContent},
|
||||
},
|
||||
Model: model,
|
||||
Messages: messages,
|
||||
}
|
||||
b, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
@@ -179,12 +187,32 @@ func (c *OpenAICompatibleClient) generateInternal(ctx context.Context, systemPro
|
||||
return "", fmt.Errorf("llm returned empty choices")
|
||||
}
|
||||
if c.log != nil {
|
||||
c.log.Infof("llm response success model=%s output_len=%d", c.model, len(out.Choices[0].Message.Content))
|
||||
c.log.Infof("llm response success model=%s output_len=%d", model, len(out.Choices[0].Message.Content))
|
||||
}
|
||||
|
||||
return out.Choices[0].Message.Content, nil
|
||||
}
|
||||
|
||||
func buildMessages(systemPrompt, userPrompt string, fileIDs []string, mode string) []chatMessage {
|
||||
mode = strings.ToLower(strings.TrimSpace(mode))
|
||||
if mode == "system_fileid_uri" {
|
||||
msgs := []chatMessage{{Role: "system", Content: systemPrompt}}
|
||||
for _, id := range fileIDs {
|
||||
if strings.TrimSpace(id) == "" {
|
||||
continue
|
||||
}
|
||||
msgs = append(msgs, chatMessage{Role: "system", Content: "fileid://" + strings.TrimSpace(id)})
|
||||
}
|
||||
msgs = append(msgs, chatMessage{Role: "user", Content: userPrompt})
|
||||
return msgs
|
||||
}
|
||||
userContent := buildUserContent(userPrompt, fileIDs)
|
||||
return []chatMessage{
|
||||
{Role: "system", Content: systemPrompt},
|
||||
{Role: "user", Content: userContent},
|
||||
}
|
||||
}
|
||||
|
||||
func buildUserContent(userPrompt string, fileIDs []string) any {
|
||||
trimmedPrompt := strings.TrimSpace(userPrompt)
|
||||
if len(fileIDs) == 0 {
|
||||
@@ -325,3 +353,31 @@ func appendIfMissing(items []string, value string) []string {
|
||||
}
|
||||
return append(items, value)
|
||||
}
|
||||
|
||||
func nonEmptyIDs(ids []string) []string {
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]string, 0, len(ids))
|
||||
seen := map[string]struct{}{}
|
||||
for _, id := range ids {
|
||||
id = strings.TrimSpace(id)
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
out = append(out, id)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (c *OpenAICompatibleClient) normalizedFilePromptMode() string {
|
||||
mode := strings.ToLower(strings.TrimSpace(c.filePromptMode))
|
||||
if mode == "system_fileid" || mode == "system_fileid_url" || mode == "system_fileid_uri" {
|
||||
return "system_fileid_uri"
|
||||
}
|
||||
return "user_content_file_parts"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user