diff --git a/Wox/go.mod b/Wox/go.mod index 3d7f9ab9f..ecf40ccb8 100644 --- a/Wox/go.mod +++ b/Wox/go.mod @@ -27,6 +27,7 @@ require ( github.com/otiai10/copy v1.14.0 github.com/parsiya/golnk v0.0.0-20221103095132-740a4c27c4ff github.com/petermattis/goid v0.0.0-20240327183114-c42a807a84ba + github.com/pkg/errors v0.9.1 github.com/robotn/gohook v0.41.0 github.com/rs/cors v1.10.1 github.com/sahilm/fuzzy v0.1.1 diff --git a/Wox/go.sum b/Wox/go.sum index a3e2cc70c..5a45bc1e8 100644 --- a/Wox/go.sum +++ b/Wox/go.sum @@ -135,6 +135,8 @@ github.com/parsiya/golnk v0.0.0-20221103095132-740a4c27c4ff h1:japdIZgV4tJIgn7Nq github.com/parsiya/golnk v0.0.0-20221103095132-740a4c27c4ff/go.mod h1:A24WXUol4NXZlK8grjh/CsZnPlimfwaQFt5PQsqS27s= github.com/petermattis/goid v0.0.0-20240327183114-c42a807a84ba h1:3jPgmsFGBID1wFfU2AbYocNcN4wqU68UaHSdMjiw/7U= github.com/petermattis/goid v0.0.0-20240327183114-c42a807a84ba/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkoukk/tiktoken-go v0.1.6 h1:JF0TlJzhTbrI30wCvFuiw6FzP2+/bR+FIxUdgEAcUsw= github.com/pkoukk/tiktoken-go v0.1.6/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/Wox/main.go b/Wox/main.go index 0c9aa2161..7ebb3defb 100644 --- a/Wox/main.go +++ b/Wox/main.go @@ -25,7 +25,6 @@ import _ "wox/plugin/host" // import all hosts import _ "wox/plugin/system" import _ "wox/plugin/system/app" import _ "wox/plugin/system/calculator" -import _ "wox/plugin/system/llm" import _ "wox/plugin/system/file" func main() { diff --git a/Wox/plugin/api.go b/Wox/plugin/api.go index 9a38fbdc5..3db445633 100644 --- a/Wox/plugin/api.go +++ b/Wox/plugin/api.go @@ -6,6 +6,7 @@ import ( "github.com/samber/lo" "path" "wox/i18n" + "wox/plugin/llm" "wox/setting" "wox/setting/definition" "wox/share" @@ -33,6 +34,8 @@ type API interface { OnSettingChanged(ctx context.Context, callback func(key string, value string)) OnGetDynamicSetting(ctx context.Context, callback func(key string) definition.PluginSettingDefinitionItem) RegisterQueryCommands(ctx context.Context, commands []MetadataCommand) + LLMChat(ctx context.Context, conversations []llm.Conversation) (string, error) + LLMChatStream(ctx context.Context, conversations []llm.Conversation) (llm.ChatStream, error) } type APIImpl struct { @@ -152,6 +155,24 @@ func (a *APIImpl) RegisterQueryCommands(ctx context.Context, commands []Metadata a.pluginInstance.SaveSetting(ctx) } +func (a *APIImpl) LLMChat(ctx context.Context, conversations []llm.Conversation) (string, error) { + provider, model := llm.GetInstance() + if provider == nil { + return "", fmt.Errorf("no LLM provider found") + } + + return provider.Chat(ctx, model, conversations) +} + +func (a *APIImpl) LLMChatStream(ctx context.Context, conversations []llm.Conversation) (llm.ChatStream, error) { + provider, model := llm.GetInstance() + if provider == nil { + return nil, fmt.Errorf("no LLM provider found") + } + + return provider.ChatStream(ctx, model, conversations) +} + func NewAPI(instance *Instance) API { apiImpl := &APIImpl{pluginInstance: instance} logFolder := path.Join(util.GetLocation().GetLogPluginDirectory(), instance.Metadata.Name) diff --git a/Wox/plugin/llm/instance.go b/Wox/plugin/llm/instance.go new file mode 100644 index 000000000..42dee358a --- /dev/null +++ b/Wox/plugin/llm/instance.go @@ -0,0 +1,17 @@ +package llm + +var provider Provider +var model Model + +func GetInstance() (Provider, Model) { + return provider, model +} + +func SetInstance(p Provider, m Model) { + provider = p + model = m +} + +func IsInstanceReady() bool { + return provider != nil && model.Name != "" +} diff --git a/Wox/plugin/llm/provider.go b/Wox/plugin/llm/provider.go new file mode 100644 index 000000000..4b0e092ff --- /dev/null +++ b/Wox/plugin/llm/provider.go @@ -0,0 +1,69 @@ +package llm + +import ( + "context" + "errors" +) + +type ConversationRole string + +var ( + ConversationRoleUser ConversationRole = "user" + ConversationRoleSystem ConversationRole = "system" +) + +type Conversation struct { + Role ConversationRole + Text string + Timestamp int64 +} + +type ModelProviderName string + +var ( + ModelProviderNameOpenAI ModelProviderName = "openai" + ModelProviderNameGoogle ModelProviderName = "google" + ModelProviderNameOllama ModelProviderName = "ollama" + ModelProviderNameGroq ModelProviderName = "groq" +) + +type Model struct { + DisplayName string + Name string + Provider ModelProviderName +} + +type Provider interface { + Close(ctx context.Context) error + ChatStream(ctx context.Context, model Model, conversations []Conversation) (ChatStream, error) + Chat(ctx context.Context, model Model, conversations []Conversation) (string, error) + Models(ctx context.Context) ([]Model, error) +} + +type ChatStream interface { + Receive(ctx context.Context) (string, error) // will return io.EOF if no more messages +} + +type ProviderConnectContext struct { + Provider ModelProviderName + + ApiKey string + Host string // E.g. "https://api.openai.com:8908" +} + +func NewProvider(ctx context.Context, connectContext ProviderConnectContext) (Provider, error) { + if connectContext.Provider == ModelProviderNameGoogle { + return NewGoogleProvider(ctx, connectContext), nil + } + if connectContext.Provider == ModelProviderNameOpenAI { + return NewOpenAIClient(ctx, connectContext), nil + } + if connectContext.Provider == ModelProviderNameOllama { + return NewOllamaProvider(ctx, connectContext), nil + } + if connectContext.Provider == ModelProviderNameGroq { + return NewGroqProvider(ctx, connectContext), nil + } + + return nil, errors.New("unknown model provider") +} diff --git a/Wox/plugin/system/llm/provider_google.go b/Wox/plugin/llm/provider_google.go similarity index 88% rename from Wox/plugin/system/llm/provider_google.go rename to Wox/plugin/llm/provider_google.go index 0eb7c61fc..4516bb662 100644 --- a/Wox/plugin/system/llm/provider_google.go +++ b/Wox/plugin/llm/provider_google.go @@ -11,7 +11,7 @@ import ( ) type GoogleProvider struct { - connectContext providerConnectContext + connectContext ProviderConnectContext client *genai.Client } @@ -20,7 +20,7 @@ type GoogleProviderStream struct { conversations []Conversation } -func NewGoogleProvider(ctx context.Context, connectContext providerConnectContext) Provider { +func NewGoogleProvider(ctx context.Context, connectContext ProviderConnectContext) Provider { return &GoogleProvider{connectContext: connectContext} } @@ -44,7 +44,7 @@ func (g *GoogleProvider) Close(ctx context.Context) error { return nil } -func (g *GoogleProvider) ChatStream(ctx context.Context, model model, conversations []Conversation) (ProviderChatStream, error) { +func (g *GoogleProvider) ChatStream(ctx context.Context, model Model, conversations []Conversation) (ChatStream, error) { if ensureClientErr := g.ensureClient(ctx); ensureClientErr != nil { return nil, ensureClientErr } @@ -57,7 +57,7 @@ func (g *GoogleProvider) ChatStream(ctx context.Context, model model, conversati return &GoogleProviderStream{conversations: conversations, stream: stream}, nil } -func (g *GoogleProvider) Chat(ctx context.Context, model model, conversations []Conversation) (string, error) { +func (g *GoogleProvider) Chat(ctx context.Context, model Model, conversations []Conversation) (string, error) { if ensureClientErr := g.ensureClient(ctx); ensureClientErr != nil { return "", ensureClientErr } @@ -80,17 +80,17 @@ func (g *GoogleProvider) Chat(ctx context.Context, model model, conversations [] return "", errors.New("no text in response") } -func (g *GoogleProvider) Models(ctx context.Context) ([]model, error) { - return []model{ +func (g *GoogleProvider) Models(ctx context.Context) ([]Model, error) { + return []Model{ { DisplayName: "google-gemini-1.0-pro", Name: "gemini-1.0-pro", - Provider: modelProviderNameGoogle, + Provider: ModelProviderNameGoogle, }, { DisplayName: "google-gemini-1.5-pro", Name: "gemini-1.5-pro", - Provider: modelProviderNameGoogle, + Provider: ModelProviderNameGoogle, }, }, nil } diff --git a/Wox/plugin/system/llm/provider_groq.go b/Wox/plugin/llm/provider_groq.go similarity index 78% rename from Wox/plugin/system/llm/provider_groq.go rename to Wox/plugin/llm/provider_groq.go index 50c8f57d7..42f063fad 100644 --- a/Wox/plugin/system/llm/provider_groq.go +++ b/Wox/plugin/llm/provider_groq.go @@ -10,22 +10,20 @@ import ( "github.com/tmc/langchaingo/llms/openai" "github.com/tmc/langchaingo/schema" "io" - "wox/plugin" "wox/util" ) type GroqProvider struct { - connectContext providerConnectContext + connectContext ProviderConnectContext client *openai.LLM } type GroqProviderStream struct { conversations []Conversation reader io.Reader - api plugin.API } -func NewGroqProvider(ctx context.Context, connectContext providerConnectContext) Provider { +func NewGroqProvider(ctx context.Context, connectContext ProviderConnectContext) Provider { return &GroqProvider{connectContext: connectContext} } @@ -33,7 +31,7 @@ func (g *GroqProvider) Close(ctx context.Context) error { return nil } -func (o *GroqProvider) ChatStream(ctx context.Context, model model, conversations []Conversation) (ProviderChatStream, error) { +func (o *GroqProvider) ChatStream(ctx context.Context, model Model, conversations []Conversation) (ChatStream, error) { client, clientErr := openai.New(openai.WithModel(model.Name), openai.WithBaseURL("https://api.groq.com/openai/v1"), openai.WithToken(o.connectContext.ApiKey)) if clientErr != nil { return nil, clientErr @@ -43,7 +41,6 @@ func (o *GroqProvider) ChatStream(ctx context.Context, model model, conversation r, w := nio.Pipe(buf) util.Go(ctx, "Groq chat stream", func() { _, err := client.GenerateContent(ctx, o.convertConversations(conversations), llms.WithStreamingFunc(func(ctx context.Context, chunk []byte) error { - o.connectContext.api.Log(ctx, plugin.LogLevelDebug, fmt.Sprintf("Groq: receive chunks from model: %s", string(chunk))) w.Write(chunk) return nil })) @@ -57,7 +54,7 @@ func (o *GroqProvider) ChatStream(ctx context.Context, model model, conversation return &GroqProviderStream{conversations: conversations, reader: r}, nil } -func (o *GroqProvider) Chat(ctx context.Context, model model, conversations []Conversation) (string, error) { +func (o *GroqProvider) Chat(ctx context.Context, model Model, conversations []Conversation) (string, error) { client, clientErr := openai.New(openai.WithModel(model.Name), openai.WithBaseURL("https://api.groq.com/openai/v1"), openai.WithToken(o.connectContext.ApiKey)) if clientErr != nil { return "", clientErr @@ -71,27 +68,27 @@ func (o *GroqProvider) Chat(ctx context.Context, model model, conversations []Co return response.Choices[0].Content, nil } -func (o *GroqProvider) Models(ctx context.Context) (models []model, err error) { - return []model{ +func (o *GroqProvider) Models(ctx context.Context) (models []Model, err error) { + return []Model{ { Name: "llama3-8b-8192", DisplayName: "llama3-8b-8192", - Provider: modelProviderNameGroq, + Provider: ModelProviderNameGroq, }, { Name: "llama3-70b-8192", DisplayName: "llama3-70b-8192", - Provider: modelProviderNameGroq, + Provider: ModelProviderNameGroq, }, { Name: "mixtral-8x7b-32768", DisplayName: "mixtral-8x7b-32768", - Provider: modelProviderNameGroq, + Provider: ModelProviderNameGroq, }, { Name: "gemma-7b-it", DisplayName: "gemma-7b-it", - Provider: modelProviderNameGroq, + Provider: ModelProviderNameGroq, }, }, nil } @@ -123,7 +120,3 @@ func (s *GroqProviderStream) Receive(ctx context.Context) (string, error) { util.GetLogger().Debug(util.NewTraceContext(), fmt.Sprintf("Groq: Send response: %s", resp)) return resp, nil } - -func (s *GroqProviderStream) Close(ctx context.Context) { - // no-op -} diff --git a/Wox/plugin/system/llm/provider_ollama.go b/Wox/plugin/llm/provider_ollama.go similarity index 81% rename from Wox/plugin/system/llm/provider_ollama.go rename to Wox/plugin/llm/provider_ollama.go index 63312d832..6cd341579 100644 --- a/Wox/plugin/system/llm/provider_ollama.go +++ b/Wox/plugin/llm/provider_ollama.go @@ -11,12 +11,11 @@ import ( "github.com/tmc/langchaingo/llms/ollama" "github.com/tmc/langchaingo/schema" "io" - "wox/plugin" "wox/util" ) type OllamaProvider struct { - connectContext providerConnectContext + connectContext ProviderConnectContext client *ollama.LLM } @@ -25,7 +24,7 @@ type OllamaProviderStream struct { reader io.Reader } -func NewOllamaProvider(ctx context.Context, connectContext providerConnectContext) Provider { +func NewOllamaProvider(ctx context.Context, connectContext ProviderConnectContext) Provider { return &OllamaProvider{connectContext: connectContext} } @@ -33,7 +32,7 @@ func (o *OllamaProvider) Close(ctx context.Context) error { return nil } -func (o *OllamaProvider) ChatStream(ctx context.Context, model model, conversations []Conversation) (ProviderChatStream, error) { +func (o *OllamaProvider) ChatStream(ctx context.Context, model Model, conversations []Conversation) (ChatStream, error) { client, clientErr := ollama.New(ollama.WithServerURL(o.connectContext.Host), ollama.WithModel(model.Name)) if clientErr != nil { return nil, clientErr @@ -43,7 +42,6 @@ func (o *OllamaProvider) ChatStream(ctx context.Context, model model, conversati r, w := nio.Pipe(buf) util.Go(ctx, "ollama chat stream", func() { _, err := client.GenerateContent(ctx, o.convertConversations(conversations), llms.WithStreamingFunc(func(ctx context.Context, chunk []byte) error { - o.connectContext.api.Log(ctx, plugin.LogLevelDebug, fmt.Sprintf("OLLAMA: receive chunks from model: %s", string(chunk))) w.Write(chunk) return nil })) @@ -57,7 +55,7 @@ func (o *OllamaProvider) ChatStream(ctx context.Context, model model, conversati return &OllamaProviderStream{conversations: conversations, reader: r}, nil } -func (o *OllamaProvider) Chat(ctx context.Context, model model, conversations []Conversation) (string, error) { +func (o *OllamaProvider) Chat(ctx context.Context, model Model, conversations []Conversation) (string, error) { client, clientErr := ollama.New(ollama.WithServerURL(o.connectContext.Host), ollama.WithModel(model.Name)) if clientErr != nil { return "", clientErr @@ -71,17 +69,17 @@ func (o *OllamaProvider) Chat(ctx context.Context, model model, conversations [] return response.Choices[0].Content, nil } -func (o *OllamaProvider) Models(ctx context.Context) (models []model, err error) { +func (o *OllamaProvider) Models(ctx context.Context) (models []Model, err error) { body, err := util.HttpGet(ctx, o.connectContext.Host+"/api/tags") if err != nil { return nil, err } gjson.Get(string(body), "models.#.name").ForEach(func(key, value gjson.Result) bool { - models = append(models, model{ + models = append(models, Model{ DisplayName: value.String(), Name: value.String(), - Provider: modelProviderNameOllama, + Provider: ModelProviderNameOllama, }) return true }) @@ -116,7 +114,3 @@ func (s *OllamaProviderStream) Receive(ctx context.Context) (string, error) { util.GetLogger().Debug(util.NewTraceContext(), fmt.Sprintf("OLLAMA: Send response: %s", resp)) return resp, nil } - -func (s *OllamaProviderStream) Close(ctx context.Context) { - // no-op -} diff --git a/Wox/plugin/system/llm/provider_openai.go b/Wox/plugin/llm/provider_openai.go similarity index 83% rename from Wox/plugin/system/llm/provider_openai.go rename to Wox/plugin/llm/provider_openai.go index ac2750e8e..73b7e286c 100644 --- a/Wox/plugin/system/llm/provider_openai.go +++ b/Wox/plugin/llm/provider_openai.go @@ -7,7 +7,7 @@ import ( ) type OpenAIProvider struct { - connectContext providerConnectContext + connectContext ProviderConnectContext client *openai.Client } @@ -16,7 +16,7 @@ type OpenAIProviderStream struct { conversations []Conversation } -func NewOpenAIClient(ctx context.Context, connectContext providerConnectContext) Provider { +func NewOpenAIClient(ctx context.Context, connectContext ProviderConnectContext) Provider { return &OpenAIProvider{connectContext: connectContext} } @@ -32,7 +32,7 @@ func (o *OpenAIProvider) ensureClient(ctx context.Context) error { return nil } -func (o *OpenAIProvider) ChatStream(ctx context.Context, model model, conversations []Conversation) (ProviderChatStream, error) { +func (o *OpenAIProvider) ChatStream(ctx context.Context, model Model, conversations []Conversation) (ChatStream, error) { if ensureClientErr := o.ensureClient(ctx); ensureClientErr != nil { return nil, ensureClientErr } @@ -49,7 +49,7 @@ func (o *OpenAIProvider) ChatStream(ctx context.Context, model model, conversati return &OpenAIProviderStream{conversations: conversations, stream: createdStream}, nil } -func (o *OpenAIProvider) Chat(ctx context.Context, model model, conversations []Conversation) (string, error) { +func (o *OpenAIProvider) Chat(ctx context.Context, model Model, conversations []Conversation) (string, error) { if ensureClientErr := o.ensureClient(ctx); ensureClientErr != nil { return "", ensureClientErr } @@ -65,12 +65,12 @@ func (o *OpenAIProvider) Chat(ctx context.Context, model model, conversations [] return resp.Choices[0].Message.Content, nil } -func (o *OpenAIProvider) Models(ctx context.Context) ([]model, error) { - return []model{ +func (o *OpenAIProvider) Models(ctx context.Context) ([]Model, error) { + return []Model{ { DisplayName: "chatgpt-3.5-turbo", Name: "gpt-3.5-turbo", - Provider: modelProviderNameOpenAI, + Provider: ModelProviderNameOpenAI, }, }, nil } @@ -78,6 +78,8 @@ func (o *OpenAIProvider) Models(ctx context.Context) ([]model, error) { func (s *OpenAIProviderStream) Receive(ctx context.Context) (string, error) { response, err := s.stream.Recv() if err != nil { + s.stream.Close() + // no more messages if err == io.EOF { return "", io.EOF @@ -92,10 +94,6 @@ func (s *OpenAIProviderStream) Receive(ctx context.Context) (string, error) { return response.Choices[0].Delta.Content, nil } -func (s *OpenAIProviderStream) Close(ctx context.Context) { - s.stream.Close() -} - func (o *OpenAIProvider) convertConversations(conversations []Conversation) []openai.ChatCompletionMessage { var chatMessages []openai.ChatCompletionMessage for _, conversation := range conversations { diff --git a/Wox/plugin/manager.go b/Wox/plugin/manager.go index fc90737ea..2cb746eb0 100644 --- a/Wox/plugin/manager.go +++ b/Wox/plugin/manager.go @@ -855,6 +855,11 @@ func (m *Manager) ExecuteRefresh(ctx context.Context, refreshableResultWithId Re newResult := resultCache.Refresh(ctx, refreshableResult) newResult = m.PolishRefreshableResult(ctx, resultCache.PluginInstance, refreshableResultWithId.ResultId, newResult) + // update result cache + resultCache.ResultTitle = newResult.Title + resultCache.ResultSubTitle = newResult.SubTitle + resultCache.ContextData = newResult.ContextData + return RefreshableResultWithResultId{ ResultId: refreshableResultWithId.ResultId, Title: newResult.Title, diff --git a/Wox/plugin/metadata.go b/Wox/plugin/metadata.go index fd4f64468..49d14d001 100644 --- a/Wox/plugin/metadata.go +++ b/Wox/plugin/metadata.go @@ -25,6 +25,9 @@ const ( // enable this feature to get query env in plugin MetadataFeatureQueryEnv MetadataFeatureName = "queryEnv" + + // enable this feature to chat with llm model in plugin + MetadataFeatureLLMChat MetadataFeatureName = "llmChat" ) // Metadata parsed from plugin.json, see `Plugin.json.md` for more detail diff --git a/Wox/plugin/system/app/app_darwin_test.go b/Wox/plugin/system/app/app_darwin_test.go index ca8d99ec0..47a90a5eb 100644 --- a/Wox/plugin/system/app/app_darwin_test.go +++ b/Wox/plugin/system/app/app_darwin_test.go @@ -48,6 +48,10 @@ func (e emptyAPIImpl) OnSettingChanged(ctx context.Context, callback func(key st func (e emptyAPIImpl) RegisterQueryCommands(ctx context.Context, commands []plugin.MetadataCommand) { } +func (e emptyAPIImpl) LLMChat(ctx context.Context, conversations []plugin.LLMConversation) (string, error) { + return "", nil +} + func TestMacRetriever_ParseAppInfo(t *testing.T) { if util.IsMacOS() { appRetriever.UpdateAPI(emptyAPIImpl{}) diff --git a/Wox/plugin/system/llm/plugin.go b/Wox/plugin/system/llm.go similarity index 92% rename from Wox/plugin/system/llm/plugin.go rename to Wox/plugin/system/llm.go index f9f093759..7cf0e8cc3 100644 --- a/Wox/plugin/system/llm/plugin.go +++ b/Wox/plugin/system/llm.go @@ -1,18 +1,20 @@ -package llm +package system import ( "context" "encoding/json" - "errors" "fmt" "github.com/samber/lo" "github.com/tidwall/gjson" - "io" "strings" + "time" "wox/plugin" + "wox/plugin/llm" "wox/setting/definition" "wox/share" "wox/util" + "wox/util/clipboard" + "wox/util/keyboard" ) var llmIcon = plugin.NewWoxImageBase64(`data:image/png;base64,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`) @@ -30,9 +32,7 @@ func init() { } type Plugin struct { - api plugin.API - provider Provider - model model + api plugin.API } func (c *Plugin) GetMetadata() plugin.Metadata { @@ -62,23 +62,23 @@ func (c *Plugin) GetMetadata() plugin.Metadata { Key: "provider", Label: "Provider", Tooltip: "The LLM service provider", - DefaultValue: string(modelProviderNameOpenAI), + DefaultValue: string(llm.ModelProviderNameOpenAI), Options: []definition.PluginSettingValueSelectOption{ { Label: "OpenAI", - Value: string(modelProviderNameOpenAI), + Value: string(llm.ModelProviderNameOpenAI), }, { Label: "Google", - Value: string(modelProviderNameGoogle), + Value: string(llm.ModelProviderNameGoogle), }, { Label: "Ollama", - Value: string(modelProviderNameOllama), + Value: string(llm.ModelProviderNameOllama), }, { Label: "Groq", - Value: string(modelProviderNameGroq), + Value: string(llm.ModelProviderNameGroq), }, }, Style: definition.PluginSettingValueStyle{ @@ -151,6 +151,9 @@ func (c *Plugin) GetMetadata() plugin.Metadata { { Name: plugin.MetadataFeatureQuerySelection, }, + { + Name: plugin.MetadataFeatureLLMChat, + }, }, } } @@ -185,7 +188,7 @@ func (c *Plugin) Init(ctx context.Context, initParams plugin.InitParams) { func (c *Plugin) Query(ctx context.Context, query plugin.Query) []plugin.QueryResult { if query.Type == plugin.QueryTypeSelection { - if c.provider == nil || query.Selection.Type == util.SelectionTypeFile { + if !llm.IsInstanceReady() || query.Selection.Type == util.SelectionTypeFile { return []plugin.QueryResult{} } @@ -218,10 +221,10 @@ func (c *Plugin) Query(ctx context.Context, query plugin.Query) []plugin.QueryRe return results } - if c.provider == nil { + if !llm.IsInstanceReady() { return []plugin.QueryResult{ { - Title: "Provider not initialized", + Title: "LLM setting not initialized", SubTitle: "Please complete the settings", Icon: llmIcon, }, @@ -243,7 +246,7 @@ func (c *Plugin) getDynamicSetting(ctx context.Context, key string) definition.P } var options []definition.PluginSettingValueSelectOption - for _, m := range c.getProviderModels(ctx, modelProviderName(provider)) { + for _, m := range c.getProviderModels(ctx, llm.ModelProviderName(provider)) { options = append(options, definition.PluginSettingValueSelectOption{ Label: m.DisplayName, Value: m.Name, @@ -265,11 +268,11 @@ func (c *Plugin) getDynamicSetting(ctx context.Context, key string) definition.P } if key == "dynamic_host" { - if c.api.GetSetting(ctx, "provider") == string(modelProviderNameOllama) { + if c.api.GetSetting(ctx, "provider") == string(llm.ModelProviderNameOllama) { return definition.PluginSettingDefinitionItem{ Type: definition.PluginSettingDefinitionTypeTextBox, Value: &definition.PluginSettingValueTextBox{ - Key: "host_" + string(modelProviderNameOllama), + Key: "host_" + string(llm.ModelProviderNameOllama), Label: "Host", Tooltip: "The Ollama host", DefaultValue: "http://localhost:11434", @@ -283,7 +286,7 @@ func (c *Plugin) getDynamicSetting(ctx context.Context, key string) definition.P } if key == "dynamic_api_key" { - if c.api.GetSetting(ctx, "provider") != string(modelProviderNameOllama) { + if c.api.GetSetting(ctx, "provider") != string(llm.ModelProviderNameOllama) { return definition.PluginSettingDefinitionItem{ Type: definition.PluginSettingDefinitionTypeTextBox, Value: &definition.PluginSettingValueTextBox{ @@ -302,10 +305,9 @@ func (c *Plugin) getDynamicSetting(ctx context.Context, key string) definition.P return definition.PluginSettingDefinitionItem{} } -func (c *Plugin) getProviderModels(ctx context.Context, providerName modelProviderName) (models []model) { - if provider, providerErr := NewProvider(ctx, providerConnectContext{ +func (c *Plugin) getProviderModels(ctx context.Context, providerName llm.ModelProviderName) (models []llm.Model) { + if provider, providerErr := llm.NewProvider(ctx, llm.ProviderConnectContext{ Provider: providerName, - api: c.api, ApiKey: c.api.GetSetting(ctx, string("api_key_"+providerName)), Host: c.api.GetSetting(ctx, string("host_"+providerName)), }); providerErr == nil { @@ -333,9 +335,8 @@ func (c *Plugin) loadClient(ctx context.Context) { return } - provider, providerErr := NewProvider(ctx, providerConnectContext{ - Provider: modelProviderName(providerName), - api: c.api, + provider, providerErr := llm.NewProvider(ctx, llm.ProviderConnectContext{ + Provider: llm.ModelProviderName(providerName), ApiKey: c.api.GetSetting(ctx, "api_key_"+providerName), Host: c.api.GetSetting(ctx, "host_"+providerName), }) @@ -345,25 +346,25 @@ func (c *Plugin) loadClient(ctx context.Context) { } //close previous provider - if c.provider != nil { - closeErr := c.provider.Close(ctx) + currentProvider, currentModel := llm.GetInstance() + if currentProvider != nil { + closeErr := currentProvider.Close(ctx) if closeErr != nil { c.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("failed to close llm provider: %s", closeErr.Error())) } else { - c.api.Log(ctx, plugin.LogLevelInfo, fmt.Sprintf("llm provider closed, model: %s", c.model.Name)) + c.api.Log(ctx, plugin.LogLevelInfo, fmt.Sprintf("llm provider closed, model: %s", currentModel.Name)) } } - models := c.getProviderModels(ctx, modelProviderName(providerName)) - var availableModel model + var availableModel llm.Model + models := c.getProviderModels(ctx, llm.ModelProviderName(providerName)) for _, m := range models { if m.Name == modelName { availableModel = m break } } - c.model = availableModel - c.provider = provider + llm.SetInstance(provider, availableModel) c.api.Log(ctx, plugin.LogLevelInfo, fmt.Sprintf("llm provider created with model: %s", modelName)) } @@ -470,17 +471,17 @@ func (c *Plugin) queryCommand(ctx context.Context, query plugin.Query) []plugin. } var prompts = strings.Split(queryTool.Prompt, "|") - var conversations []Conversation + var conversations []llm.Conversation for index, message := range prompts { msg := fmt.Sprintf(message, query.Search) if index%2 == 0 { - conversations = append(conversations, Conversation{ - Role: ConversationRoleUser, + conversations = append(conversations, llm.Conversation{ + Role: llm.ConversationRoleUser, Text: msg, }) } else { - conversations = append(conversations, Conversation{ - Role: ConversationRoleSystem, + conversations = append(conversations, llm.Conversation{ + Role: llm.ConversationRoleSystem, Text: msg, }) } @@ -490,6 +491,7 @@ func (c *Plugin) queryCommand(ctx context.Context, query plugin.Query) []plugin. current.Icon = llmLoadingIcon current.Preview.PreviewData += deltaAnswer current.Preview.ScrollPosition = plugin.WoxPreviewScrollPositionBottom + current.ContextData = current.Preview.PreviewData return current } onAnswerErr := func(current plugin.RefreshableResult, err error) plugin.RefreshableResult { @@ -504,74 +506,38 @@ func (c *Plugin) queryCommand(ctx context.Context, query plugin.Query) []plugin. return current } + _, model := llm.GetInstance() return []plugin.QueryResult{{ Title: fmt.Sprintf("Chat with %s", query.Command), - SubTitle: fmt.Sprintf("%s - %s", c.model.Provider, c.model.DisplayName), + SubTitle: fmt.Sprintf("%s - %s", model.Provider, model.DisplayName), Preview: plugin.WoxPreview{PreviewType: plugin.WoxPreviewTypeMarkdown, PreviewData: ""}, Icon: llmLoadingIcon, RefreshInterval: 100, - OnRefresh: c.generateGptResultRefresh(ctx, conversations, onAnswering, onAnswerErr, onAnswerFinished), + OnRefresh: createLLMOnRefreshHandler(ctx, c.api.LLMChatStream, conversations, func() bool { + return true + }, onAnswering, onAnswerErr, onAnswerFinished), + Actions: []plugin.QueryResultAction{ + { + Name: "Copy", + Action: func(ctx context.Context, actionContext plugin.ActionContext) { + clipboard.WriteText(actionContext.ContextData) + }, + }, + { + Name: "Copy and Paste to active app", + Action: func(ctx context.Context, actionContext plugin.ActionContext) { + clipboard.WriteText(actionContext.ContextData) + util.Go(context.Background(), "clipboard to copy", func() { + time.Sleep(time.Millisecond * 100) + err := keyboard.SimulatePaste() + if err != nil { + c.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("simulate paste clipboard failed, err=%s", err.Error())) + } else { + c.api.Log(ctx, plugin.LogLevelInfo, "simulate paste clipboard success") + } + }) + }, + }, + }, }} } - -// generate a result which will send chat messages to openai and show the result automatically -func (c *Plugin) generateGptResultRefresh(ctx context.Context, conversations []Conversation, - onAnswering func(plugin.RefreshableResult, string) plugin.RefreshableResult, - onAnswerErr func(plugin.RefreshableResult, error) plugin.RefreshableResult, - onAnswerFinished func(plugin.RefreshableResult) plugin.RefreshableResult) func(ctx context.Context, current plugin.RefreshableResult) plugin.RefreshableResult { - - var stream ProviderChatStream - var creatingStream bool - return func(ctx context.Context, current plugin.RefreshableResult) plugin.RefreshableResult { - if stream == nil { - if creatingStream { - c.api.Log(ctx, plugin.LogLevelInfo, "Already creating stream, waiting create finish") - return current - } - - startTime := util.GetSystemTimestamp() - c.api.Log(ctx, plugin.LogLevelInfo, "creating stream") - creatingStream = true - createdStream, createErr := c.provider.ChatStream(ctx, c.model, conversations) - creatingStream = false - c.api.Log(ctx, plugin.LogLevelInfo, fmt.Sprintf("created stream (cost %d ms)", util.GetSystemTimestamp()-startTime)) - if createErr != nil { - if onAnswerErr != nil { - current = onAnswerErr(current, createErr) - } - current.RefreshInterval = 0 // stop refreshing - return current - } - stream = createdStream - } - - c.api.Log(ctx, plugin.LogLevelInfo, fmt.Sprintf("reading stream, model=%s", c.model.Name)) - response, streamErr := stream.Receive(ctx) - if errors.Is(streamErr, io.EOF) { - c.api.Log(ctx, plugin.LogLevelInfo, "read stream completed") - stream.Close(ctx) - if onAnswerFinished != nil { - current = onAnswerFinished(current) - } - current.RefreshInterval = 0 // stop refreshing - return current - } - - if streamErr != nil { - c.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("failed to read stream: %s", streamErr.Error())) - stream.Close(ctx) - if onAnswerErr != nil { - current = onAnswerErr(current, streamErr) - } - current.RefreshInterval = 0 // stop refreshing - return current - } - - if onAnswering != nil { - c.api.Log(ctx, plugin.LogLevelInfo, fmt.Sprintf("streamed %d text", len(response))) - current = onAnswering(current, response) - } - - return current - } -} diff --git a/Wox/plugin/system/llm/provider.go b/Wox/plugin/system/llm/provider.go deleted file mode 100644 index d2d5a31e0..000000000 --- a/Wox/plugin/system/llm/provider.go +++ /dev/null @@ -1,79 +0,0 @@ -package llm - -import ( - "context" - "errors" - "wox/plugin" -) - -type ConversationRole string - -var ( - ConversationRoleUser ConversationRole = "user" - ConversationRoleSystem ConversationRole = "system" -) - -type Conversation struct { - Role ConversationRole - Text string - Timestamp int64 -} - -type modelProviderName string - -var ( - modelProviderNameOpenAI modelProviderName = "openai" - modelProviderNameGoogle modelProviderName = "google" - modelProviderNameOllama modelProviderName = "ollama" - modelProviderNameGroq modelProviderName = "groq" -) - -var modelProviderNames = []modelProviderName{ - modelProviderNameOpenAI, - modelProviderNameGoogle, - modelProviderNameOllama, - modelProviderNameGroq, -} - -type model struct { - DisplayName string - Name string - Provider modelProviderName -} - -type providerConnectContext struct { - Provider modelProviderName - api plugin.API - - ApiKey string - Host string // E.g. "https://api.openai.com:8908" -} - -type Provider interface { - Close(ctx context.Context) error - ChatStream(ctx context.Context, model model, conversations []Conversation) (ProviderChatStream, error) - Chat(ctx context.Context, model model, conversations []Conversation) (string, error) - Models(ctx context.Context) ([]model, error) -} - -type ProviderChatStream interface { - Receive(ctx context.Context) (string, error) // will return io.EOF if no more messages - Close(ctx context.Context) -} - -func NewProvider(ctx context.Context, connectContext providerConnectContext) (Provider, error) { - if connectContext.Provider == modelProviderNameGoogle { - return NewGoogleProvider(ctx, connectContext), nil - } - if connectContext.Provider == modelProviderNameOpenAI { - return NewOpenAIClient(ctx, connectContext), nil - } - if connectContext.Provider == modelProviderNameOllama { - return NewOllamaProvider(ctx, connectContext), nil - } - if connectContext.Provider == modelProviderNameGroq { - return NewGroqProvider(ctx, connectContext), nil - } - - return nil, errors.New("unknown model provider") -} diff --git a/Wox/plugin/system/theme.go b/Wox/plugin/system/theme.go index e5f755628..2647d4038 100644 --- a/Wox/plugin/system/theme.go +++ b/Wox/plugin/system/theme.go @@ -2,9 +2,15 @@ package system import ( "context" + "encoding/json" + "fmt" + "github.com/google/uuid" "github.com/samber/lo" "wox/plugin" + "wox/plugin/llm" + "wox/resource" "wox/share" + "wox/util" ) var themeIcon = plugin.NewWoxImageBase64(`data:image/png;base64,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`) @@ -32,7 +38,12 @@ func (c *ThemePlugin) GetMetadata() plugin.Metadata { TriggerKeywords: []string{ "theme", }, - Commands: []plugin.MetadataCommand{}, + Commands: []plugin.MetadataCommand{ + { + Command: "ai", + Description: "Generate a new theme with AI", + }, + }, SupportedOS: []string{ "Windows", "Macos", @@ -46,6 +57,10 @@ func (c *ThemePlugin) Init(ctx context.Context, initParams plugin.InitParams) { } func (c *ThemePlugin) Query(ctx context.Context, query plugin.Query) []plugin.QueryResult { + if query.Command == "ai" { + return c.queryAI(ctx, query) + } + ui := plugin.GetPluginManager().GetUI() return lo.FilterMap(ui.GetAllThemes(ctx), func(theme share.Theme, _ int) (plugin.QueryResult, bool) { match, _ := IsStringMatchScore(ctx, theme.ThemeName, query.Search) @@ -68,3 +83,107 @@ func (c *ThemePlugin) Query(ctx context.Context, query plugin.Query) []plugin.Qu } }) } + +func (c *ThemePlugin) queryAI(ctx context.Context, query plugin.Query) []plugin.QueryResult { + if query.Search == "" { + return []plugin.QueryResult{ + { + Title: "Please describe the theme you want to generate", + Icon: themeIcon, + }, + } + } + + embedThemes := resource.GetEmbedThemes(ctx) + if len(embedThemes) == 0 { + return []plugin.QueryResult{ + { + Title: "No embed theme found", + Icon: themeIcon, + }, + } + } + + exampleThemeJson := embedThemes[0] + + var conversations []llm.Conversation + conversations = append(conversations, llm.Conversation{ + Role: llm.ConversationRoleUser, + Text: ` +我正在编写Wox的主题,该主题是由一段json组成,例如:` + exampleThemeJson + ` + +现在我想让你根据上面的格式生成一个新的主题,主题的要求是:` + query.Search + `。 + +有一些注意点需要你遵守: +1. 你的回答结果必须是JSON格式,且只能回答json相关内容,忽略解释,注释等信息 +2. 主题名称你自己决定,主题ID为随机生成的UUID +3. 主题作者统一为:Wox launcher AI +4. IsSystemTheme字段必须为false +`, + }) + + onAnswering := func(current plugin.RefreshableResult, deltaAnswer string) plugin.RefreshableResult { + current.SubTitle = "Generating..." + current.Preview.PreviewData += deltaAnswer + current.Preview.ScrollPosition = plugin.WoxPreviewScrollPositionBottom + current.ContextData = current.Preview.PreviewData + return current + } + onAnswerErr := func(current plugin.RefreshableResult, err error) plugin.RefreshableResult { + current.Preview.PreviewData += fmt.Sprintf("\n\nError: %s", err.Error()) + current.RefreshInterval = 0 // stop refreshing + return current + } + onAnswerFinished := func(current plugin.RefreshableResult) plugin.RefreshableResult { + current.RefreshInterval = 0 // stop refreshing + current.Title = "Theme generated" + util.Go(ctx, "theme generated", func() { + themeJson := current.ContextData + if themeJson == "" { + return + } + + // use regex to get json snippet from the whole text + group := util.FindRegexGroup(`(?ms){(?P.*?)}`, themeJson) + if len(group) == 0 { + c.api.Notify(ctx, "Failed to extract json", "") + return + } + + var jsonTheme = fmt.Sprintf("{%s}", group["json"]) + var theme share.Theme + unmarshalErr := json.Unmarshal([]byte(jsonTheme), &theme) + if unmarshalErr != nil { + c.api.Notify(ctx, "Failed to unmarshal theme json", unmarshalErr.Error()) + return + } + + theme.ThemeId = uuid.NewString() + plugin.GetPluginManager().GetUI().InstallTheme(ctx, theme) + }) + return current + } + + startGenerate := false + return []plugin.QueryResult{ + { + Title: "Generate theme with ai", + SubTitle: "Enter to generate", + Icon: themeIcon, + Preview: plugin.WoxPreview{PreviewType: plugin.WoxPreviewTypeMarkdown, PreviewData: ""}, + RefreshInterval: 100, + OnRefresh: createLLMOnRefreshHandler(ctx, c.api.LLMChatStream, conversations, func() bool { + return startGenerate + }, onAnswering, onAnswerErr, onAnswerFinished), + Actions: []plugin.QueryResultAction{ + { + Name: "Apply", + PreventHideAfterAction: true, + Action: func(ctx context.Context, actionContext plugin.ActionContext) { + startGenerate = true + }, + }, + }, + }, + } +} diff --git a/Wox/plugin/system/util.go b/Wox/plugin/system/util.go index 462b204a9..a72df897f 100644 --- a/Wox/plugin/system/util.go +++ b/Wox/plugin/system/util.go @@ -3,6 +3,7 @@ package system import ( "context" "crypto/md5" + "errors" "fmt" "github.com/disintegration/imaging" "github.com/mat/besticon/besticon" @@ -11,6 +12,7 @@ import ( "os" "path" "wox/plugin" + "wox/plugin/llm" "wox/setting" "wox/util" ) @@ -94,3 +96,69 @@ func getWebsiteIconWithCache(ctx context.Context, websiteUrl string) (plugin.Wox return woxImage, nil } + +func createLLMOnRefreshHandler(ctx context.Context, + chatStream func(ctx context.Context, conversations []llm.Conversation) (llm.ChatStream, error), + conversations []llm.Conversation, + shouldStartAnswering func() bool, + onAnswering func(plugin.RefreshableResult, string) plugin.RefreshableResult, + onAnswerErr func(plugin.RefreshableResult, error) plugin.RefreshableResult, + onAnswerFinished func(plugin.RefreshableResult) plugin.RefreshableResult) func(ctx context.Context, current plugin.RefreshableResult) plugin.RefreshableResult { + + var stream llm.ChatStream + var creatingStream bool + return func(ctx context.Context, current plugin.RefreshableResult) plugin.RefreshableResult { + if !shouldStartAnswering() { + return current + } + + if stream == nil { + if creatingStream { + util.GetLogger().Info(ctx, "Already creating stream, waiting create finish") + return current + } + + startTime := util.GetSystemTimestamp() + util.GetLogger().Info(ctx, "creating stream") + creatingStream = true + createdStream, createErr := chatStream(ctx, conversations) + creatingStream = false + util.GetLogger().Info(ctx, fmt.Sprintf("created stream (cost %d ms)", util.GetSystemTimestamp()-startTime)) + if createErr != nil { + if onAnswerErr != nil { + current = onAnswerErr(current, createErr) + } + current.RefreshInterval = 0 // stop refreshing + return current + } + stream = createdStream + } + + util.GetLogger().Info(ctx, fmt.Sprintf("reading stream")) + response, streamErr := stream.Receive(ctx) + if errors.Is(streamErr, io.EOF) { + util.GetLogger().Info(ctx, "read stream completed") + if onAnswerFinished != nil { + current = onAnswerFinished(current) + } + current.RefreshInterval = 0 // stop refreshing + return current + } + + if streamErr != nil { + util.GetLogger().Info(ctx, fmt.Sprintf("failed to read stream: %s", streamErr.Error())) + if onAnswerErr != nil { + current = onAnswerErr(current, streamErr) + } + current.RefreshInterval = 0 // stop refreshing + return current + } + + if onAnswering != nil { + util.GetLogger().Info(ctx, fmt.Sprintf("streamed %d text", len(response))) + current = onAnswering(current, response) + } + + return current + } +} diff --git a/Wox/resource/resource.go b/Wox/resource/resource.go index 28fcca8f4..2bc3b090e 100644 --- a/Wox/resource/resource.go +++ b/Wox/resource/resource.go @@ -92,9 +92,6 @@ func parseThemes(ctx context.Context) error { if err != nil { return err } - if err != nil { - return err - } if len(dir) == 0 { return fmt.Errorf("no theme file found") } diff --git a/Wox/share/ui.go b/Wox/share/ui.go index a16d3791d..7fc9192c9 100644 --- a/Wox/share/ui.go +++ b/Wox/share/ui.go @@ -45,6 +45,7 @@ type UI interface { GetServerPort(ctx context.Context) int GetAllThemes(ctx context.Context) []Theme ChangeTheme(ctx context.Context, theme Theme) + InstallTheme(ctx context.Context, theme Theme) } type ShowContext struct { diff --git a/Wox/ui/ui_impl.go b/Wox/ui/ui_impl.go index 3e46749ce..84faff7ac 100644 --- a/Wox/ui/ui_impl.go +++ b/Wox/ui/ui_impl.go @@ -58,6 +58,11 @@ func (u *uiImpl) ChangeTheme(ctx context.Context, theme share.Theme) { u.invokeWebsocketMethod(ctx, "ChangeTheme", theme) } +func (u *uiImpl) InstallTheme(ctx context.Context, theme share.Theme) { + logger.Info(ctx, fmt.Sprintf("install theme: %s", theme.ThemeName)) + GetUIManager().AddTheme(ctx, theme) +} + func (u *uiImpl) OpenSettingWindow(ctx context.Context, windowContext share.SettingWindowContext) { u.invokeWebsocketMethod(ctx, "OpenSettingWindow", windowContext) }