Compare commits

...

24 Commits

Author SHA1 Message Date
github-actions[bot]
47cf24e19d Update version to v1.4.239 and commit 2025-07-07 18:58:38 +00:00
Kayvan Sylvan
3f07afbef4 Merge pull request #1592 from ksylvan/0707-possible-go-routine-race-condition-fix
Fix Streaming Error Handling in Chatter
2025-07-07 11:57:11 -07:00
Kayvan Sylvan
38d714dccd chore: improve error comparison in TestChatter_Send_StreamingErrorPropagation 2025-07-07 11:20:01 -07:00
Kayvan Sylvan
d0b5c95d61 chore: remove redundant channel closure in Send method
### CHANGES

- Remove redundant `close(responseChan)` in `Send` method
- Update `SendStream` to close `responseChan` properly
- Modify test to reflect channel closure logic
2025-07-07 11:02:04 -07:00
Kayvan Sylvan
f8f80ca206 chore: rename doneChan to done and add streaming aggregation test
## CHANGES

- Rename `doneChan` variable to `done` for consistency
- Add `streamChunks` field to mock vendor struct
- Implement chunk sending logic in mock SendStream method
- Add comprehensive streaming success aggregation test case
- Verify message aggregation from multiple stream chunks
- Test assistant response role and content validation
- Ensure proper session handling in streaming scenarios
2025-07-07 10:49:29 -07:00
Kayvan Sylvan
0af458872f feat: add test for Chatter's Send method error propagation
### CHANGES

- Implement mockVendor for testing ai.Vendor interface
- Add TestChatter_Send_StreamingErrorPropagation test case
- Verify error propagation in Chatter's Send method
- Ensure session returns even on streaming error
- Create temporary database for testing Chatter functionality
2025-07-07 10:36:40 -07:00
Kayvan Sylvan
24e46a6f37 chore: rename channels for clarity in Send method
### CHANGES

- Rename `done` to `doneChan` for clarity
- Adjust channel closure for `doneChan`
- Update channel listening logic to use `doneChan`
2025-07-07 10:28:54 -07:00
Kayvan Sylvan
d6a31e68b0 refactor: rename channel variable to responseChan for better clarity in streaming logic
## CHANGES

- Rename `channel` variable to `responseChan` for clarity
- Update channel references in goroutine defer statements
- Pass renamed channel to `SendStream` method call
- Maintain consistent naming throughout streaming flow
2025-07-07 10:23:42 -07:00
Kayvan Sylvan
b1013ca61b chore: close channel after sending stream in Send
### CHANGES

- Add `channel` closure after sending stream
- Ensure resource cleanup in `Send` method
2025-07-07 10:09:24 -07:00
Kayvan Sylvan
6b4ce946a5 chore: refactor error handling and response aggregation in Send
### CHANGES

- Simplify response aggregation loop in `Send`
- Remove redundant select case for closed channel
- Streamline error checking from `errChan`
- Ensure goroutine completion before returning
2025-07-07 09:39:58 -07:00
Kayvan Sylvan
2d2830e9c8 chore: enhance Chatter.Send method with proper goroutine synchronization
### CHANGES
- Add `done` channel to track goroutine completion.
- Replace `errChan` closure with `done` channel closure.
- Ensure main loop waits for goroutine on channel close.
- Synchronize error handling with `done` channel wait.
2025-07-07 09:09:04 -07:00
Kayvan Sylvan
115327fdab refactor: use select to handle stream and error channels concurrently
### CHANGES

- Replace for-range loop with a non-blocking select statement.
- Process message and error channels concurrently for better handling.
- Improve the robustness of streaming error detection.
- Exit loop cleanly when the message channel closes.
2025-07-07 08:37:31 -07:00
Kayvan Sylvan
e672f9b73f chore: simplify error handling in streaming chat response by removing unnecessary select statement 2025-07-07 08:15:24 -07:00
Kayvan Sylvan
ef4364a1aa fix: improve error handling in streaming chat functionality
## CHANGES

- Add dedicated error channel for stream operations
- Separate error handling from message streaming logic
- Check for streaming errors after channel closure
- Close error channel properly in goroutine cleanup
- Remove error messages from message stream channel
- Add proper error propagation for stream failures
2025-07-07 03:31:58 -07:00
github-actions[bot]
cb3f8ed43d Update version to v1.4.238 and commit 2025-07-07 10:24:00 +00:00
Kayvan Sylvan
4c1803cb6d Merge pull request #1591 from ksylvan/0707-anthropic-can-now-use-only-oauth
Improved Anthropic Plugin Configuration Logic
2025-07-07 03:22:27 -07:00
Kayvan Sylvan
d1c614d44e refactor: extract vendor token identifier constant and remove redundant configure call
## CHANGES

- Extract vendor token identifier into named constant
- Remove redundant Configure() call from IsConfigured method
- Use constant for token validation consistency
- Improve code maintainability with centralized identifier
2025-07-07 03:16:45 -07:00
Kayvan Sylvan
dbaa0b9754 feat: add vendor configuration validation and OAuth auto-authentication
## CHANGES

- Add IsConfigured check to vendor configuration loop
- Implement IsConfigured method for Anthropic client validation
- Remove conditional API key requirement based on OAuth
- Add automatic OAuth flow when no valid token
- Validate both API key and OAuth token configurations
- Simplify API key setup question logic
- Add token expiration checking with 5-minute buffer
2025-07-07 02:49:27 -07:00
github-actions[bot]
4cfe2375ab Update version to v1.4.237 and commit 2025-07-07 03:05:51 +00:00
Kayvan Sylvan
2b371b69c7 Merge pull request #1590 from ksylvan/0706-webui-topp-fix
Do not pass non-default TopP values
2025-07-06 20:04:16 -07:00
Kayvan Sylvan
6222a613e4 fix: add conditional check for TopP parameter in OpenAI client
## CHANGES

- Add zero-value check before setting TopP parameter
- Prevent sending TopP when value is zero
- Apply fix to both chat completions method
- Apply fix to response parameters method
- Ensure consistent parameter handling across OpenAI calls
2025-07-06 19:53:21 -07:00
github-actions[bot]
0882c43532 Update version to v1.4.236 and commit 2025-07-06 19:39:24 +00:00
Kayvan Sylvan
f0e1a1b77f Merge pull request #1587 from ksylvan/0705-enhance-bug-report-template
Enhance bug report template
2025-07-06 12:37:56 -07:00
Kayvan Sylvan
a774f991ab chore: enhance bug report template with detailed system info and installation method fields
## CHANGES

- Add detailed instructions for bug reproduction steps
- Include operating system dropdown with specific architectures
- Add OS version textarea with command examples
- Create installation method dropdown with all options
- Replace version checkbox with proper version output field
- Improve formatting and organization of form sections
- Add helpful links to installation documentation
2025-07-06 11:01:53 -07:00
9 changed files with 299 additions and 21 deletions

View File

@@ -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:

View File

@@ -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
View 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)
}
}

View File

@@ -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)
}
}

View File

@@ -1 +1 @@
"1.4.235"
"1.4.239"

View File

@@ -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

View File

@@ -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))
}

View File

@@ -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))
}

View File

@@ -1,3 +1,3 @@
package main
var version = "v1.4.235"
var version = "v1.4.239"