mirror of
https://github.com/danielmiessler/Fabric.git
synced 2026-01-09 22:38:10 -05:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47cf24e19d | ||
|
|
3f07afbef4 | ||
|
|
38d714dccd | ||
|
|
d0b5c95d61 | ||
|
|
f8f80ca206 | ||
|
|
0af458872f | ||
|
|
24e46a6f37 | ||
|
|
d6a31e68b0 | ||
|
|
b1013ca61b | ||
|
|
6b4ce946a5 | ||
|
|
2d2830e9c8 | ||
|
|
115327fdab | ||
|
|
e672f9b73f | ||
|
|
ef4364a1aa | ||
|
|
cb3f8ed43d | ||
|
|
4c1803cb6d | ||
|
|
d1c614d44e | ||
|
|
dbaa0b9754 | ||
|
|
4cfe2375ab | ||
|
|
2b371b69c7 | ||
|
|
6222a613e4 | ||
|
|
0882c43532 | ||
|
|
f0e1a1b77f | ||
|
|
a774f991ab |
59
.github/ISSUE_TEMPLATE/bug.yml
vendored
59
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -7,29 +7,74 @@ body:
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
Please provide as much detail as possible to help us understand and reproduce the issue.
|
||||
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: Also tell us, what did you expect to happen?
|
||||
placeholder: Tell us what you see!
|
||||
value: "I was doing THIS, when THAT happened. I was expecting THAT_OTHER_THING to happen instead."
|
||||
value: "Please provide all the steps to reproduce the bug. I was doing THIS, when THAT happened. I was expecting THAT_OTHER_THING to happen instead."
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
|
||||
- type: dropdown
|
||||
id: os
|
||||
attributes:
|
||||
label: Operating System
|
||||
options:
|
||||
- macOS - Silicon (arm64)
|
||||
- macOS - Intel (amd64)
|
||||
- Linux - amd64
|
||||
- Linux - arm64
|
||||
- Windows
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: os-version
|
||||
attributes:
|
||||
label: OS Version
|
||||
description: Please provide details about your OS version by running one of the following commands.
|
||||
placeholder: |
|
||||
macOS: `sw_vers`
|
||||
Linux: `uname -a` or `cat /etc/os-release`
|
||||
Windows: `ver`
|
||||
render: shell
|
||||
|
||||
- type: dropdown
|
||||
id: installation
|
||||
attributes:
|
||||
label: How did you install Fabric?
|
||||
description: "Please select the method you used to install Fabric. You can find this information in the [Installation section of the README](https://github.com/ksylvan/fabric/blob/main/README.md#installation)."
|
||||
options:
|
||||
- Release Binary - Windows
|
||||
- Release Binary - macOS (arm64)
|
||||
- Release Binary - macOS (amd64)
|
||||
- Release Binary - Linux (amd64)
|
||||
- Release Binary - Linux (arm64)
|
||||
- Package Manager - Homebrew (macOS)
|
||||
- Package Manager - AUR (Arch Linux)
|
||||
- From Source
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: version
|
||||
attributes:
|
||||
label: Version check
|
||||
description: Please make sure you were using the latest version of this project available in the `main` branch.
|
||||
options:
|
||||
- label: Yes I was.
|
||||
required: true
|
||||
label: Version
|
||||
description: Please copy and paste the output of `fabric --version` (or `fabric-ai --version` if you installed it via brew) here.
|
||||
render: text
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
||||
|
||||
- type: textarea
|
||||
id: screens
|
||||
attributes:
|
||||
|
||||
@@ -66,17 +66,35 @@ func (o *Chatter) Send(request *common.ChatRequest, opts *common.ChatOptions) (s
|
||||
message := ""
|
||||
|
||||
if o.Stream {
|
||||
channel := make(chan string)
|
||||
responseChan := make(chan string)
|
||||
errChan := make(chan error, 1)
|
||||
done := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
if streamErr := o.vendor.SendStream(session.GetVendorMessages(), opts, channel); streamErr != nil {
|
||||
channel <- streamErr.Error()
|
||||
defer close(done)
|
||||
if streamErr := o.vendor.SendStream(session.GetVendorMessages(), opts, responseChan); streamErr != nil {
|
||||
errChan <- streamErr
|
||||
}
|
||||
}()
|
||||
|
||||
for response := range channel {
|
||||
for response := range responseChan {
|
||||
message += response
|
||||
fmt.Print(response)
|
||||
}
|
||||
|
||||
// Wait for goroutine to finish
|
||||
<-done
|
||||
|
||||
// Check for errors in errChan
|
||||
select {
|
||||
case streamErr := <-errChan:
|
||||
if streamErr != nil {
|
||||
err = streamErr
|
||||
return
|
||||
}
|
||||
default:
|
||||
// No errors, continue
|
||||
}
|
||||
} else {
|
||||
if message, err = o.vendor.Send(context.Background(), session.GetVendorMessages(), opts); err != nil {
|
||||
return
|
||||
|
||||
181
core/chatter_test.go
Normal file
181
core/chatter_test.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/danielmiessler/fabric/chat"
|
||||
"github.com/danielmiessler/fabric/common"
|
||||
"github.com/danielmiessler/fabric/plugins/db/fsdb"
|
||||
)
|
||||
|
||||
// mockVendor implements the ai.Vendor interface for testing
|
||||
type mockVendor struct {
|
||||
sendStreamError error
|
||||
streamChunks []string
|
||||
}
|
||||
|
||||
func (m *mockVendor) GetName() string {
|
||||
return "mock"
|
||||
}
|
||||
|
||||
func (m *mockVendor) GetSetupDescription() string {
|
||||
return "mock vendor"
|
||||
}
|
||||
|
||||
func (m *mockVendor) IsConfigured() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *mockVendor) Configure() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockVendor) Setup() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockVendor) SetupFillEnvFileContent(*bytes.Buffer) {
|
||||
}
|
||||
|
||||
func (m *mockVendor) ListModels() ([]string, error) {
|
||||
return []string{"test-model"}, nil
|
||||
}
|
||||
|
||||
func (m *mockVendor) SendStream(messages []*chat.ChatCompletionMessage, opts *common.ChatOptions, responseChan chan string) error {
|
||||
// Send chunks if provided (for successful streaming test)
|
||||
if m.streamChunks != nil {
|
||||
for _, chunk := range m.streamChunks {
|
||||
responseChan <- chunk
|
||||
}
|
||||
}
|
||||
// Close the channel like real vendors do
|
||||
close(responseChan)
|
||||
return m.sendStreamError
|
||||
}
|
||||
|
||||
func (m *mockVendor) Send(ctx context.Context, messages []*chat.ChatCompletionMessage, opts *common.ChatOptions) (string, error) {
|
||||
return "test response", nil
|
||||
}
|
||||
|
||||
func (m *mockVendor) NeedsRawMode(modelName string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func TestChatter_Send_StreamingErrorPropagation(t *testing.T) {
|
||||
// Create a temporary database for testing
|
||||
tempDir := t.TempDir()
|
||||
db := fsdb.NewDb(tempDir)
|
||||
|
||||
// Create a mock vendor that will return an error from SendStream
|
||||
expectedError := errors.New("streaming error")
|
||||
mockVendor := &mockVendor{
|
||||
sendStreamError: expectedError,
|
||||
}
|
||||
|
||||
// Create chatter with streaming enabled
|
||||
chatter := &Chatter{
|
||||
db: db,
|
||||
Stream: true, // Enable streaming to trigger SendStream path
|
||||
vendor: mockVendor,
|
||||
model: "test-model",
|
||||
}
|
||||
|
||||
// Create a test request
|
||||
request := &common.ChatRequest{
|
||||
Message: &chat.ChatCompletionMessage{
|
||||
Role: chat.ChatMessageRoleUser,
|
||||
Content: "test message",
|
||||
},
|
||||
}
|
||||
|
||||
// Create test options
|
||||
opts := &common.ChatOptions{
|
||||
Model: "test-model",
|
||||
}
|
||||
|
||||
// Call Send and expect it to return the streaming error
|
||||
session, err := chatter.Send(request, opts)
|
||||
|
||||
// Verify that the error from SendStream is propagated
|
||||
if err == nil {
|
||||
t.Fatal("Expected error to be returned, but got nil")
|
||||
}
|
||||
|
||||
if !errors.Is(err, expectedError) {
|
||||
t.Errorf("Expected error %q, but got %q", expectedError, err)
|
||||
}
|
||||
|
||||
// Session should still be returned (it was built successfully before the streaming error)
|
||||
if session == nil {
|
||||
t.Error("Expected session to be returned even when streaming error occurs")
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatter_Send_StreamingSuccessfulAggregation(t *testing.T) {
|
||||
// Create a temporary database for testing
|
||||
tempDir := t.TempDir()
|
||||
db := fsdb.NewDb(tempDir)
|
||||
|
||||
// Create test chunks that should be aggregated
|
||||
testChunks := []string{"Hello", " ", "world", "!", " This", " is", " a", " test."}
|
||||
expectedMessage := "Hello world! This is a test."
|
||||
|
||||
// Create a mock vendor that will send chunks successfully
|
||||
mockVendor := &mockVendor{
|
||||
sendStreamError: nil, // No error for successful streaming
|
||||
streamChunks: testChunks,
|
||||
}
|
||||
|
||||
// Create chatter with streaming enabled
|
||||
chatter := &Chatter{
|
||||
db: db,
|
||||
Stream: true, // Enable streaming to trigger SendStream path
|
||||
vendor: mockVendor,
|
||||
model: "test-model",
|
||||
}
|
||||
|
||||
// Create a test request
|
||||
request := &common.ChatRequest{
|
||||
Message: &chat.ChatCompletionMessage{
|
||||
Role: chat.ChatMessageRoleUser,
|
||||
Content: "test message",
|
||||
},
|
||||
}
|
||||
|
||||
// Create test options
|
||||
opts := &common.ChatOptions{
|
||||
Model: "test-model",
|
||||
}
|
||||
|
||||
// Call Send and expect successful aggregation
|
||||
session, err := chatter.Send(request, opts)
|
||||
|
||||
// Verify no error occurred
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, but got: %v", err)
|
||||
}
|
||||
|
||||
// Verify session was returned
|
||||
if session == nil {
|
||||
t.Fatal("Expected session to be returned")
|
||||
}
|
||||
|
||||
// Verify the message was aggregated correctly
|
||||
messages := session.GetVendorMessages()
|
||||
if len(messages) != 2 { // user message + assistant response
|
||||
t.Fatalf("Expected 2 messages, got %d", len(messages))
|
||||
}
|
||||
|
||||
// Check the assistant's response (last message)
|
||||
assistantMessage := messages[len(messages)-1]
|
||||
if assistantMessage.Role != chat.ChatMessageRoleAssistant {
|
||||
t.Errorf("Expected assistant role, got %s", assistantMessage.Role)
|
||||
}
|
||||
|
||||
if assistantMessage.Content != expectedMessage {
|
||||
t.Errorf("Expected aggregated message %q, got %q", expectedMessage, assistantMessage.Content)
|
||||
}
|
||||
}
|
||||
@@ -243,7 +243,7 @@ func (o *PluginRegistry) SetupVendor(vendorName string) (err error) {
|
||||
func (o *PluginRegistry) ConfigureVendors() {
|
||||
o.VendorManager.Clear()
|
||||
for _, vendor := range o.VendorsAll.Vendors {
|
||||
if vendorErr := vendor.Configure(); vendorErr == nil {
|
||||
if vendorErr := vendor.Configure(); vendorErr == nil && vendor.IsConfigured() {
|
||||
o.VendorManager.AddVendors(vendor)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
"1.4.235"
|
||||
"1.4.239"
|
||||
|
||||
@@ -19,6 +19,8 @@ const webSearchToolName = "web_search"
|
||||
const webSearchToolType = "web_search_20250305"
|
||||
const sourcesHeader = "## Sources"
|
||||
|
||||
const vendorTokenIdentifier = "claude"
|
||||
|
||||
func NewClient() (ret *Client) {
|
||||
vendorName := "Anthropic"
|
||||
ret = &Client{}
|
||||
@@ -32,11 +34,7 @@ func NewClient() (ret *Client) {
|
||||
ret.ApiBaseURL = ret.AddSetupQuestion("API Base URL", false)
|
||||
ret.ApiBaseURL.Value = defaultBaseUrl
|
||||
ret.UseOAuth = ret.AddSetupQuestionBool("Use OAuth login", false)
|
||||
if plugins.ParseBoolElseFalse(ret.UseOAuth.Value) {
|
||||
ret.ApiKey = ret.PluginBase.AddSetupQuestion("API key", false)
|
||||
} else {
|
||||
ret.ApiKey = ret.PluginBase.AddSetupQuestion("API key", true)
|
||||
}
|
||||
ret.ApiKey = ret.PluginBase.AddSetupQuestion("API key", false)
|
||||
|
||||
ret.maxTokens = 4096
|
||||
ret.defaultRequiredUserMessage = "Hi"
|
||||
@@ -52,6 +50,38 @@ func NewClient() (ret *Client) {
|
||||
return
|
||||
}
|
||||
|
||||
// IsConfigured returns true if either the API key or OAuth is configured
|
||||
func (an *Client) IsConfigured() bool {
|
||||
// Check if API key is configured
|
||||
if an.ApiKey.Value != "" {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if OAuth is enabled and has a valid token
|
||||
if plugins.ParseBoolElseFalse(an.UseOAuth.Value) {
|
||||
storage, err := common.NewOAuthStorage()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// If no valid token exists, automatically run OAuth flow
|
||||
if !storage.HasValidToken(vendorTokenIdentifier, 5) {
|
||||
fmt.Println("OAuth enabled but no valid token found. Starting authentication...")
|
||||
_, err := RunOAuthFlow()
|
||||
if err != nil {
|
||||
fmt.Printf("OAuth authentication failed: %v\n", err)
|
||||
return false
|
||||
}
|
||||
// After successful OAuth flow, check again
|
||||
return storage.HasValidToken("claude", 5)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
*plugins.PluginBase
|
||||
ApiBaseURL *plugins.SetupQuestion
|
||||
|
||||
@@ -69,7 +69,9 @@ func (o *Client) buildChatCompletionParams(
|
||||
|
||||
if !opts.Raw {
|
||||
ret.Temperature = openai.Float(opts.Temperature)
|
||||
ret.TopP = openai.Float(opts.TopP)
|
||||
if opts.TopP != 0 {
|
||||
ret.TopP = openai.Float(opts.TopP)
|
||||
}
|
||||
if opts.MaxTokens != 0 {
|
||||
ret.MaxTokens = openai.Int(int64(opts.MaxTokens))
|
||||
}
|
||||
|
||||
@@ -221,7 +221,9 @@ func (o *Client) buildResponseParams(
|
||||
|
||||
if !opts.Raw {
|
||||
ret.Temperature = openai.Float(opts.Temperature)
|
||||
ret.TopP = openai.Float(opts.TopP)
|
||||
if opts.TopP != 0 {
|
||||
ret.TopP = openai.Float(opts.TopP)
|
||||
}
|
||||
if opts.MaxTokens != 0 {
|
||||
ret.MaxOutputTokens = openai.Int(int64(opts.MaxTokens))
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
package main
|
||||
|
||||
var version = "v1.4.235"
|
||||
var version = "v1.4.239"
|
||||
|
||||
Reference in New Issue
Block a user