Compare commits

..

5 Commits

Author SHA1 Message Date
Kujtim Hoxha 4415220555 fix minor issue 2025-04-27 19:24:46 +02:00
Kujtim Hoxha a3a04d8a54 fix gemini provider 2025-04-27 19:12:02 +02:00
Lukáš Loukota 792e2b164b fix: gemini tool calling 2025-04-27 19:12:02 +02:00
Kujtim Hoxha 5859dcdc00 small glob fixes 2025-04-27 18:01:31 +02:00
isaac-scarrott 3c2b0f4dd0 [feature/ripgrep-glob] Add ripgrep-based file globbing to improve performance
- Introduced `globWithRipgrep` function to perform file globbing using the `rg` (ripgrep) command.
- Updated `globFiles` to prioritize ripgrep-based globbing and fall back to doublestar-based globbing if ripgrep fails.
- Added logic to handle ripgrep command execution, output parsing, and filtering of hidden files.
- Ensured results are sorted by path length and limited to the specified maximum number of matches.
- Modified imports to include `os/exec` and `bytes` for ripgrep integration.
2025-04-27 18:01:31 +02:00
2 changed files with 121 additions and 83 deletions
+49 -76
View File
@@ -54,19 +54,6 @@ func newGeminiClient(opts providerClientOptions) GeminiClient {
func (g *geminiClient) convertMessages(messages []message.Message) []*genai.Content {
var history []*genai.Content
// Add system message first
history = append(history, &genai.Content{
Parts: []genai.Part{genai.Text(g.providerOptions.systemMessage)},
Role: "user",
})
// Add a system response to acknowledge the system message
history = append(history, &genai.Content{
Parts: []genai.Part{genai.Text("I'll help you with that.")},
Role: "model",
})
for _, msg := range messages {
switch msg.Role {
case message.User:
@@ -132,7 +119,8 @@ func (g *geminiClient) convertMessages(messages []message.Message) []*genai.Cont
}
func (g *geminiClient) convertTools(tools []tools.BaseTool) []*genai.Tool {
geminiTools := make([]*genai.Tool, 0, len(tools))
geminiTool := &genai.Tool{}
geminiTool.FunctionDeclarations = make([]*genai.FunctionDeclaration, 0, len(tools))
for _, tool := range tools {
info := tool.Info()
@@ -146,23 +134,18 @@ func (g *geminiClient) convertTools(tools []tools.BaseTool) []*genai.Tool {
},
}
geminiTools = append(geminiTools, &genai.Tool{
FunctionDeclarations: []*genai.FunctionDeclaration{declaration},
})
geminiTool.FunctionDeclarations = append(geminiTool.FunctionDeclarations, declaration)
}
return geminiTools
return []*genai.Tool{geminiTool}
}
func (g *geminiClient) finishReason(reason genai.FinishReason) message.FinishReason {
reasonStr := reason.String()
switch {
case reasonStr == "STOP":
case reason == genai.FinishReasonStop:
return message.FinishReasonEndTurn
case reasonStr == "MAX_TOKENS":
case reason == genai.FinishReasonMaxTokens:
return message.FinishReasonMaxTokens
case strings.Contains(reasonStr, "FUNCTION") || strings.Contains(reasonStr, "TOOL"):
return message.FinishReasonToolUse
default:
return message.FinishReasonUnknown
}
@@ -171,7 +154,11 @@ func (g *geminiClient) finishReason(reason genai.FinishReason) message.FinishRea
func (g *geminiClient) send(ctx context.Context, messages []message.Message, tools []tools.BaseTool) (*ProviderResponse, error) {
model := g.client.GenerativeModel(g.providerOptions.model.APIModel)
model.SetMaxOutputTokens(int32(g.providerOptions.maxTokens))
model.SystemInstruction = &genai.Content{
Parts: []genai.Part{
genai.Text(g.providerOptions.systemMessage),
},
}
// Convert tools
if len(tools) > 0 {
model.Tools = g.convertTools(tools)
@@ -189,19 +176,13 @@ func (g *geminiClient) send(ctx context.Context, messages []message.Message, too
attempts := 0
for {
attempts++
var toolCalls []message.ToolCall
chat := model.StartChat()
chat.History = geminiMessages[:len(geminiMessages)-1] // All but last message
lastMsg := geminiMessages[len(geminiMessages)-1]
var lastText string
for _, part := range lastMsg.Parts {
if text, ok := part.(genai.Text); ok {
lastText = string(text)
break
}
}
resp, err := chat.SendMessage(ctx, genai.Text(lastText))
resp, err := chat.SendMessage(ctx, lastMsg.Parts...)
// If there is an error we are going to see if we can retry the call
if err != nil {
retry, after, retryErr := g.shouldRetry(attempts, err)
@@ -221,7 +202,6 @@ func (g *geminiClient) send(ctx context.Context, messages []message.Message, too
}
content := ""
var toolCalls []message.ToolCall
if len(resp.Candidates) > 0 && resp.Candidates[0].Content != nil {
for _, part := range resp.Candidates[0].Content.Parts {
@@ -232,20 +212,28 @@ func (g *geminiClient) send(ctx context.Context, messages []message.Message, too
id := "call_" + uuid.New().String()
args, _ := json.Marshal(p.Args)
toolCalls = append(toolCalls, message.ToolCall{
ID: id,
Name: p.Name,
Input: string(args),
Type: "function",
ID: id,
Name: p.Name,
Input: string(args),
Type: "function",
Finished: true,
})
}
}
}
finishReason := message.FinishReasonEndTurn
if len(resp.Candidates) > 0 {
finishReason = g.finishReason(resp.Candidates[0].FinishReason)
}
if len(toolCalls) > 0 {
finishReason = message.FinishReasonToolUse
}
return &ProviderResponse{
Content: content,
ToolCalls: toolCalls,
Usage: g.usage(resp),
FinishReason: g.finishReason(resp.Candidates[0].FinishReason),
FinishReason: finishReason,
}, nil
}
}
@@ -253,7 +241,11 @@ func (g *geminiClient) send(ctx context.Context, messages []message.Message, too
func (g *geminiClient) stream(ctx context.Context, messages []message.Message, tools []tools.BaseTool) <-chan ProviderEvent {
model := g.client.GenerativeModel(g.providerOptions.model.APIModel)
model.SetMaxOutputTokens(int32(g.providerOptions.maxTokens))
model.SystemInstruction = &genai.Content{
Parts: []genai.Part{
genai.Text(g.providerOptions.systemMessage),
},
}
// Convert tools
if len(tools) > 0 {
model.Tools = g.convertTools(tools)
@@ -277,18 +269,10 @@ func (g *geminiClient) stream(ctx context.Context, messages []message.Message, t
for {
attempts++
chat := model.StartChat()
chat.History = geminiMessages[:len(geminiMessages)-1] // All but last message
chat.History = geminiMessages[:len(geminiMessages)-1]
lastMsg := geminiMessages[len(geminiMessages)-1]
var lastText string
for _, part := range lastMsg.Parts {
if text, ok := part.(genai.Text); ok {
lastText = string(text)
break
}
}
iter := chat.SendMessageStream(ctx, genai.Text(lastText))
iter := chat.SendMessageStream(ctx, lastMsg.Parts...)
currentContent := ""
toolCalls := []message.ToolCall{}
@@ -331,23 +315,23 @@ func (g *geminiClient) stream(ctx context.Context, messages []message.Message, t
for _, part := range resp.Candidates[0].Content.Parts {
switch p := part.(type) {
case genai.Text:
newText := string(p)
delta := newText[len(currentContent):]
delta := string(p)
if delta != "" {
eventChan <- ProviderEvent{
Type: EventContentDelta,
Content: delta,
}
currentContent = newText
currentContent += delta
}
case genai.FunctionCall:
id := "call_" + uuid.New().String()
args, _ := json.Marshal(p.Args)
newCall := message.ToolCall{
ID: id,
Name: p.Name,
Input: string(args),
Type: "function",
ID: id,
Name: p.Name,
Input: string(args),
Type: "function",
Finished: true,
}
isNew := true
@@ -369,37 +353,26 @@ func (g *geminiClient) stream(ctx context.Context, messages []message.Message, t
eventChan <- ProviderEvent{Type: EventContentStop}
if finalResp != nil {
finishReason := message.FinishReasonEndTurn
if len(finalResp.Candidates) > 0 {
finishReason = g.finishReason(finalResp.Candidates[0].FinishReason)
}
if len(toolCalls) > 0 {
finishReason = message.FinishReasonToolUse
}
eventChan <- ProviderEvent{
Type: EventComplete,
Response: &ProviderResponse{
Content: currentContent,
ToolCalls: toolCalls,
Usage: g.usage(finalResp),
FinishReason: g.finishReason(finalResp.Candidates[0].FinishReason),
FinishReason: finishReason,
},
}
return
}
// If we get here, we need to retry
if attempts > maxRetries {
eventChan <- ProviderEvent{
Type: EventError,
Error: fmt.Errorf("maximum retry attempts reached: %d retries", maxRetries),
}
return
}
// Wait before retrying
select {
case <-ctx.Done():
if ctx.Err() != nil {
eventChan <- ProviderEvent{Type: EventError, Error: ctx.Err()}
}
return
case <-time.After(time.Duration(2000*(1<<(attempts-1))) * time.Millisecond):
continue
}
}
}()
+72 -7
View File
@@ -1,11 +1,13 @@
package tools
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/fs"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
@@ -132,14 +134,73 @@ func (g *globTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
}
func globFiles(pattern, searchPath string, limit int) ([]string, bool, error) {
if !strings.HasPrefix(pattern, "/") && !strings.HasPrefix(pattern, searchPath) {
if !strings.HasSuffix(searchPath, "/") {
searchPath += "/"
}
pattern = searchPath + pattern
matches, err := globWithRipgrep(pattern, searchPath, limit)
if err == nil {
return matches, len(matches) >= limit, nil
}
fsys := os.DirFS("/")
return globWithDoublestar(pattern, searchPath, limit)
}
func globWithRipgrep(
pattern, searchRoot string,
limit int,
) ([]string, error) {
if searchRoot == "" {
searchRoot = "."
}
rgBin, err := exec.LookPath("rg")
if err != nil {
return nil, fmt.Errorf("ripgrep not found in $PATH: %w", err)
}
if !filepath.IsAbs(pattern) && !strings.HasPrefix(pattern, "/") {
pattern = "/" + pattern
}
args := []string{
"--files",
"--null",
"--glob", pattern,
"-L",
}
cmd := exec.Command(rgBin, args...)
cmd.Dir = searchRoot
out, err := cmd.CombinedOutput()
if err != nil {
if ee, ok := err.(*exec.ExitError); ok && ee.ExitCode() == 1 {
return nil, nil
}
return nil, fmt.Errorf("ripgrep: %w\n%s", err, out)
}
var matches []string
for _, p := range bytes.Split(out, []byte{0}) {
if len(p) == 0 {
continue
}
abs := filepath.Join(searchRoot, string(p))
if skipHidden(abs) {
continue
}
matches = append(matches, abs)
}
sort.SliceStable(matches, func(i, j int) bool {
return len(matches[i]) < len(matches[j])
})
if len(matches) > limit {
matches = matches[:limit]
}
return matches, nil
}
func globWithDoublestar(pattern, searchPath string, limit int) ([]string, bool, error) {
fsys := os.DirFS(searchPath)
relPattern := strings.TrimPrefix(pattern, "/")
@@ -158,7 +219,11 @@ func globFiles(pattern, searchPath string, limit int) ([]string, bool, error) {
return nil // Skip files we can't access
}
absPath := "/" + path // Restore absolute path
absPath := path // Restore absolute path
if !strings.HasPrefix(absPath, searchPath) {
absPath = filepath.Join(searchPath, absPath)
}
matches = append(matches, fileInfo{
path: absPath,
modTime: info.ModTime(),