From 2ca3a65a20a34c4005250279eb792742e85bb3fe Mon Sep 17 00:00:00 2001 From: teddy Date: Sun, 22 Sep 2024 04:33:17 +0200 Subject: [PATCH] feat(translate) : add deepl sdk for translate --- CHANGELOG.md | 2 + commands/text.go | 6 +-- commands/translate.go | 70 +++++++++++++++++++++++++++++++ lang/en.go | 47 +++++++++++---------- lang/fr.go | 47 +++++++++++---------- main.go | 1 + sdk/claude.go | 4 +- sdk/deepl.go | 87 ++++++++++++++++++++++++++++++++++++++ sdk/openai.go | 4 +- sdk/sdk.go | 91 ++-------------------------------------- sdk/text.go | 89 ++++++++++++++++++++++++++++++++++++++- sdk/translate.go | 98 +++++++++++++++++++++++++++++++++++++++++++ service/translate.go | 67 +++++++++++++++++++++++++++++ 13 files changed, 476 insertions(+), 137 deletions(-) create mode 100644 commands/translate.go create mode 100644 sdk/deepl.go create mode 100644 sdk/translate.go create mode 100644 service/translate.go diff --git a/CHANGELOG.md b/CHANGELOG.md index ab62ff2..83d302d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### Added +* Add interactive mode for translate command +* Add sdk deepl for translate command * Add interactive mode for text command * Add parameter for text command * `sdk` : Select the sdk to use diff --git a/commands/text.go b/commands/text.go index f3b744f..4ea7133 100644 --- a/commands/text.go +++ b/commands/text.go @@ -21,11 +21,11 @@ func TextCommand() *cli.Command { ArgsUsage: "[prompt|-]", Aliases: []string{"t"}, Action: textAction, - Flags: TextFlags(), + Flags: textFlags(), } } -func TextFlags() []cli.Flag { +func textFlags() []cli.Flag { l := lang.GetLocalize() textSdk := sdk.GetSdkText() @@ -33,7 +33,7 @@ func TextFlags() []cli.Flag { &cli.StringFlag{ Name: "sdk", Aliases: []string{"S"}, - Usage: l.Get("text-sdk-usage"), + Usage: l.Get("sdk-usage"), DefaultText: textSdk.GetName(), Action: func(c *cli.Context, value string) error { if err := sdk.InitSdkText(value); err != nil { diff --git a/commands/translate.go b/commands/translate.go new file mode 100644 index 0000000..8927c44 --- /dev/null +++ b/commands/translate.go @@ -0,0 +1,70 @@ +package commands + +import ( + "github.com/LordPax/aicli/lang" + "github.com/LordPax/aicli/sdk" + "github.com/LordPax/aicli/service" + cli "github.com/urfave/cli/v2" +) + +func TranslateCommand() *cli.Command { + l := lang.GetLocalize() + return &cli.Command{ + Name: "translate", + Usage: l.Get("translate-usage"), + ArgsUsage: "[text|-]", + Aliases: []string{"tr"}, + Action: translateAction, + Flags: translateFlags(), + } +} + +func translateFlags() []cli.Flag { + l := lang.GetLocalize() + sdkTranslate := sdk.GetSdkTranslate() + return []cli.Flag{ + &cli.StringFlag{ + Name: "sdk", + Aliases: []string{"S"}, + Usage: l.Get("sdk-usage"), + DefaultText: sdkTranslate.GetName(), + Action: func(c *cli.Context, value string) error { + if err := sdk.InitSdkTranslate(value); err != nil { + return err + } + return nil + }, + }, + &cli.StringFlag{ + Name: "source", + Aliases: []string{"s"}, + Usage: l.Get("translate-source-usage"), + Action: func(c *cli.Context, value string) error { + sdkTranslate.SetSourceLang(value) + return nil + }, + }, + &cli.StringFlag{ + Name: "target", + Aliases: []string{"t"}, + Usage: l.Get("translate-target-usage"), + Action: func(c *cli.Context, value string) error { + sdkTranslate.SetTargetLang(value) + return nil + }, + }, + } +} + +func translateAction(c *cli.Context) error { + text := c.Args().First() + + if c.NArg() == 0 { + if err := service.TranslateInteractiveMode(); err != nil { + return err + } + return nil + } + + return service.TranslateText(text) +} diff --git a/lang/en.go b/lang/en.go index 087dbc1..fafd1ae 100644 --- a/lang/en.go +++ b/lang/en.go @@ -3,25 +3,30 @@ package lang import "github.com/LordPax/aicli/utils" var EN_STRINGS = LangString{ - "usage": "CLI toot to use ai model", - "output-desc": "Output directory", - "output-dir-empty": "Output directory is empty", - "silent": "Disable printing log to stdout", - "no-args": "No arguments provided", - "no-command": "No command provided", - "unknown-sdk": "Unknown sdk \"%s\"", - "sdk-model-usage": "Select a model", - "text-usage": "Generate text from a prompt", - "text-sdk-usage": "Select a sdk", - "text-temp-usage": "Set temperature", - "text-system-usage": "Instruction with role system (use \"-\" for stdin)", - "text-history-usage": "Select a history", - "text-clear-usage": "Clear history", - "text-file-usage": "Text file to use", - "text-input": "(\"exit\" to quit) " + utils.Blue + "user> " + utils.Reset, - "text-list-history-usage": "List history", - "type-required": "Type is required", - "apiKey-required": "API key is required", - "empty-file": "File \"%s\" is empty", - "empty-history": "History \"%s\" is empty\n", + "usage": "CLI toot to use ai model", + "output-desc": "Output directory", + "output-dir-empty": "Output directory is empty", + "silent": "Disable printing log to stdout", + "no-args": "No arguments provided", + "no-command": "No command provided", + "unknown-sdk": "Unknown sdk \"%s\"", + "sdk-model-usage": "Select a model", + "text-usage": "Generate text from a prompt", + "sdk-usage": "Select a sdk", + "text-temp-usage": "Set temperature", + "text-system-usage": "Instruction with role system (use \"-\" for stdin)", + "text-history-usage": "Select a history", + "text-clear-usage": "Clear history", + "text-file-usage": "Text file to use", + "text-input": "(\"exit\" to quit) " + utils.Blue + "user> " + utils.Reset, + "translate-input": "(\"exit\" to quit) " + utils.Blue + "> " + utils.Reset, + "text-list-history-usage": "List history", + "type-required": "Type is required", + "apiKey-required": "API key is required", + "empty-file": "File \"%s\" is empty", + "empty-history": "History \"%s\" is empty\n", + "translate-usage": "Translate a text", + "translate-source-usage": "Source language", + "translate-target-usage": "Target language", + "translate-target-required": "Target language is required", } diff --git a/lang/fr.go b/lang/fr.go index 25173d0..345f2ad 100644 --- a/lang/fr.go +++ b/lang/fr.go @@ -3,25 +3,30 @@ package lang import "github.com/LordPax/aicli/utils" var FR_STRINGS = LangString{ - "usage": "CLI pour utiliser des modèles d'IA", - "output-desc": "Répertoire de sortie", - "output-dir-empty": "Le répertoire de sortie est vide", - "silent": "Désactiver l'impression du journal sur stdout", - "no-args": "Aucun argument fourni", - "no-command": "Aucune commande fournie", - "unknown-sdk": "Sdk inconnu \"%s\"", - "sdk-model-usage": "Sélectionner un modèle", - "text-usage": "Générer du texte à partir d'un prompt", - "text-sdk-usage": "Sélectionner un sdk", - "text-temp-usage": "Définir la température", - "text-system-usage": "Instruction avec rôle système (utilisez \"-\" pour stdin)", - "text-history-usage": "Sélectionner un historique", - "text-clear-usage": "Effacer l'historique", - "text-file-usage": "Fichier texte à utiliser", - "text-input": "(\"exit\" pour quitter) " + utils.Blue + "user> " + utils.Reset, - "text-list-history-usage": "Lister l'historique", - "type-required": "Le type est requis", - "apiKey-required": "La clé API est requise", - "empty-file": "Le fichier est vide", - "empty-history": "L'historique \"%s\" est vide\n", + "usage": "CLI pour utiliser des modèles d'IA", + "output-desc": "Répertoire de sortie", + "output-dir-empty": "Le répertoire de sortie est vide", + "silent": "Désactiver l'impression du journal sur stdout", + "no-args": "Aucun argument fourni", + "no-command": "Aucune commande fournie", + "unknown-sdk": "Sdk inconnu \"%s\"", + "sdk-model-usage": "Sélectionner un modèle", + "text-usage": "Générer du texte à partir d'un prompt", + "sdk-usage": "Sélectionner un sdk", + "text-temp-usage": "Définir la température", + "text-system-usage": "Instruction avec rôle système (utilisez \"-\" pour stdin)", + "text-history-usage": "Sélectionner un historique", + "text-clear-usage": "Effacer l'historique", + "text-file-usage": "Fichier texte à utiliser", + "text-input": "(\"exit\" pour quitter) " + utils.Blue + "user> " + utils.Reset, + "translate-input": "(\"exit\" pour quitter) " + utils.Blue + "> " + utils.Reset, + "text-list-history-usage": "Lister l'historique", + "type-required": "Le type est requis", + "apiKey-required": "La clé API est requise", + "empty-file": "Le fichier \"%s\" est vide", + "empty-history": "L'historique \"%s\" est vide\n", + "translate-usage": "Traduire un texte", + "translate-source-usage": "Langue source", + "translate-target-usage": "Langue cible", + "translate-target-required": "La langue cible est requise", } diff --git a/main.go b/main.go index ff758a7..85ba26c 100644 --- a/main.go +++ b/main.go @@ -51,6 +51,7 @@ func main() { app.Flags = commands.MainFlags() app.Commands = []*cli.Command{ commands.TextCommand(), + commands.TranslateCommand(), // TODO : add command for image, audio and translate } diff --git a/sdk/claude.go b/sdk/claude.go index 3ab93e7..4be1418 100644 --- a/sdk/claude.go +++ b/sdk/claude.go @@ -42,10 +42,10 @@ func NewClaudeText(apiKey, model string, temp float64) (*ClaudeText, error) { Name: "claude", ApiUrl: "https://api.anthropic.com/v1/messages", ApiKey: apiKey, - Model: "claude-3-5-sonnet-20240620", }, SdkText: SdkText{ - Temp: 0.7, + Model: "claude-3-5-sonnet-20240620", + Temp: 0.7, }, TextHistory: *history, } diff --git a/sdk/deepl.go b/sdk/deepl.go new file mode 100644 index 0000000..595e912 --- /dev/null +++ b/sdk/deepl.go @@ -0,0 +1,87 @@ +package sdk + +import ( + "encoding/json" + "errors" + "io" + "net/http" + + "github.com/LordPax/aicli/utils" +) + +type DeepLBody struct { + Text []string `json:"text"` + TargetLang string `json:"target_lang"` + SourceLang string `json:"source_lang"` +} + +type DeepLResponse struct { + Translations []struct { + DetectedSourceLanguage string `json:"detected_source_language"` + Text string `json:"text"` + } `json:"translations"` +} + +type DeepLError struct { + Message string `json:"message"` +} + +type DeepL struct { + Sdk + SdkTranslate +} + +func NewDeepL(apiKey string) (*DeepL, error) { + return &DeepL{ + Sdk: Sdk{ + Name: "deepl", + ApiUrl: "https://api-free.deepl.com/v2/translate", + ApiKey: apiKey, + }, + SdkTranslate: SdkTranslate{ + SourceLang: "", + TargetLang: "", + }, + }, nil +} + +func (d *DeepL) SendRequest(text string) (string, error) { + var deeplResponse DeepLResponse + + jsonBody, err := json.Marshal(DeepLBody{ + Text: []string{text}, + TargetLang: d.GetTargetLang(), + SourceLang: d.GetSourceLang(), + }) + if err != nil { + return "", err + } + + resp, err := utils.PostRequest(d.ApiUrl, jsonBody, map[string]string{ + "Content-Type": "application/json", + "Authorization": "DeepL-Auth-Key " + d.ApiKey, + }) + if err != nil { + return "", err + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + + if resp.StatusCode != http.StatusOK { + var deepLError DeepLError + if err := json.Unmarshal(respBody, &deepLError); err != nil { + return "", err + } + return "", errors.New(deepLError.Message) + } + + if err := json.Unmarshal(respBody, &deeplResponse); err != nil { + return "", err + } + + return deeplResponse.Translations[0].Text, nil +} diff --git a/sdk/openai.go b/sdk/openai.go index 2014553..13d4fef 100644 --- a/sdk/openai.go +++ b/sdk/openai.go @@ -50,10 +50,10 @@ func NewOpenaiText(apiKey, model string, temp float64) (*OpenaiText, error) { Name: "openai", ApiUrl: "https://api.openai.com/v1/chat/completions", ApiKey: apiKey, - Model: "gpt-4", }, SdkText: SdkText{ - Temp: 0.7, + Model: "gpt-4", + Temp: 0.7, }, TextHistory: *history, } diff --git a/sdk/sdk.go b/sdk/sdk.go index bd17175..e799583 100644 --- a/sdk/sdk.go +++ b/sdk/sdk.go @@ -1,19 +1,6 @@ package sdk -import ( - "errors" - "fmt" - - "github.com/LordPax/aicli/config" - "github.com/LordPax/aicli/lang" -) - -var sdkTextInstance ITextService - type ISdk interface { - SendRequest(text string) (Message, error) - SetModel(model string) - GetModel() string GetName() string } @@ -21,92 +8,22 @@ type Sdk struct { Name string ApiUrl string ApiKey string - Model string } func (s *Sdk) GetName() string { return s.Name } -func (s *Sdk) SetModel(model string) { - s.Model = model -} - -func (s *Sdk) GetModel() string { - return s.Model -} - -func InitSdkText(sdk string) error { - var err error - - l := lang.GetLocalize() - sdkType, apiKey, model, temp, err := getConfigText(sdk) - if err != nil { +func InitSdk() error { + if err := InitSdkText(""); err != nil { return err } - switch sdkType { - case "openai": - sdkTextInstance, err = NewOpenaiText(apiKey, model, temp) - case "claude": - sdkTextInstance, err = NewClaudeText(apiKey, model, temp) - default: - return fmt.Errorf(l.Get("unknown-sdk"), sdkType) - } - - if err != nil { + if err := InitSdkTranslate(""); err != nil { return err } - return nil -} - -func getConfigText(sdkType string) (string, string, string, float64, error) { - l := lang.GetLocalize() - confText := config.CONFIG_INI.Section("text") - - if sdkType == "" { - sdkType = confText.Key("type").String() - if sdkType == "" { - return "", "", "", 0, errors.New(l.Get("type-required")) - } - } - - apiKey := confText.Key("apiKey").String() - if apiKey == "" { - apiKey = confText.Key(sdkType + "-apiKey").String() - if apiKey == "" { - return "", "", "", 0, errors.New(l.Get("api-key-required")) - } - } - - model := confText.Key("model").String() - if model == "" { - model = confText.Key(sdkType + "-model").String() - } - - temp, _ := confText.Key("temp").Float64() - if temp == 0 { - temp, _ = confText.Key(sdkType + "-temp").Float64() - } - - return sdkType, apiKey, model, temp, nil -} - -func InitSdk() error { - if err := InitSdkText(""); err != nil { - return err - } - - // TODO : init sdk for image, audio and translate + // TODO : init sdk for image, audio return nil } - -func GetSdkText() ITextService { - return sdkTextInstance -} - -func SetSdkText(s ITextService) { - sdkTextInstance = s -} diff --git a/sdk/text.go b/sdk/text.go index 54d18fe..e8c5581 100644 --- a/sdk/text.go +++ b/sdk/text.go @@ -1,5 +1,15 @@ package sdk +import ( + "errors" + "fmt" + + "github.com/LordPax/aicli/config" + "github.com/LordPax/aicli/lang" +) + +var sdkTextInstance ITextService + type Content struct { Type string `json:"type"` Text string `json:"text"` @@ -45,12 +55,89 @@ type ITextService interface { } type ISdkText interface { + SendRequest(text string) (Message, error) + SetModel(model string) + GetModel() string SetTemp(temp float64) GetTemp() float64 } type SdkText struct { - Temp float64 + Model string + Temp float64 +} + +func InitSdkText(sdk string) error { + var err error + + l := lang.GetLocalize() + sdkType, apiKey, model, temp, err := getConfigText(sdk) + if err != nil { + return err + } + + switch sdkType { + case "openai": + sdkTextInstance, err = NewOpenaiText(apiKey, model, temp) + case "claude": + sdkTextInstance, err = NewClaudeText(apiKey, model, temp) + default: + return fmt.Errorf(l.Get("unknown-sdk"), sdkType) + } + + if err != nil { + return err + } + + return nil +} + +func getConfigText(sdkType string) (string, string, string, float64, error) { + l := lang.GetLocalize() + confText := config.CONFIG_INI.Section("text") + + if sdkType == "" { + sdkType = confText.Key("type").String() + if sdkType == "" { + return "", "", "", 0, errors.New(l.Get("type-required")) + } + } + + apiKey := confText.Key("apiKey").String() + if apiKey == "" { + apiKey = confText.Key(sdkType + "-apiKey").String() + if apiKey == "" { + return "", "", "", 0, errors.New(l.Get("api-key-required")) + } + } + + model := confText.Key("model").String() + if model == "" { + model = confText.Key(sdkType + "-model").String() + } + + temp, _ := confText.Key("temp").Float64() + if temp == 0 { + temp, _ = confText.Key(sdkType + "-temp").Float64() + } + + return sdkType, apiKey, model, temp, nil +} + +func GetSdkText() ITextService { + return sdkTextInstance +} + +func SetSdkText(s ITextService) { + sdkTextInstance = s +} + +func (s *SdkText) SetModel(model string) { + s.Model = model +} + +func (s *SdkText) GetModel() string { + return s.Model } func (s *SdkText) SetTemp(temp float64) { diff --git a/sdk/translate.go b/sdk/translate.go new file mode 100644 index 0000000..b1118a9 --- /dev/null +++ b/sdk/translate.go @@ -0,0 +1,98 @@ +package sdk + +import ( + "errors" + "fmt" + + "github.com/LordPax/aicli/config" + "github.com/LordPax/aicli/lang" +) + +var sdkTranslateInstance ITranslateService + +type ITranslateService interface { + ISdk + ISdkTranslate +} + +type ISdkTranslate interface { + SendRequest(text string) (string, error) + SetSourceLang(source string) + GetSourceLang() string + SetTargetLang(target string) + GetTargetLang() string +} + +type SdkTranslate struct { + SourceLang string + TargetLang string +} + +func InitSdkTranslate(sdk string) error { + var err error + + l := lang.GetLocalize() + sdkType, apiKey, err := getConfigTranslate(sdk) + if err != nil { + return err + } + + switch sdkType { + case "deepl": + sdkTranslateInstance, err = NewDeepL(apiKey) + default: + return fmt.Errorf(l.Get("unknown-sdk"), sdkType) + } + + if err != nil { + return err + } + + return nil +} + +func getConfigTranslate(sdkType string) (string, string, error) { + l := lang.GetLocalize() + configTranslate := config.CONFIG_INI.Section("translate") + + if sdkType == "" { + sdkType = configTranslate.Key("type").String() + if sdkType == "" { + return "", "", errors.New(l.Get("type-required")) + } + } + + apiKey := configTranslate.Key("apiKey").String() + if apiKey == "" { + apiKey = configTranslate.Key(sdkType + "-apiKey").String() + if apiKey == "" { + return "", "", fmt.Errorf(l.Get("api-key-required"), sdkType) + } + } + + return sdkType, apiKey, nil +} + +func GetSdkTranslate() ITranslateService { + return sdkTranslateInstance +} + +func SetSdkTranslate(s ITranslateService) { + sdkTranslateInstance = s +} + +func (s *SdkTranslate) SetSourceLang(source string) { + s.SourceLang = source +} + +func (s *SdkTranslate) GetSourceLang() string { + return s.SourceLang +} + +func (s *SdkTranslate) SetTargetLang(target string) { + s.TargetLang = target +} + +func (s *SdkTranslate) GetTargetLang() string { + return s.TargetLang +} diff --git a/service/translate.go b/service/translate.go new file mode 100644 index 0000000..96105a3 --- /dev/null +++ b/service/translate.go @@ -0,0 +1,67 @@ +package service + +import ( + "errors" + "fmt" + "io" + "os" + "strings" + + "github.com/LordPax/aicli/lang" + "github.com/LordPax/aicli/sdk" + "github.com/LordPax/aicli/utils" +) + +func TranslateText(text string) error { + sdkTranslate := sdk.GetSdkTranslate() + l := lang.GetLocalize() + + if text == "-" { + stdin, err := io.ReadAll(os.Stdin) + if err != nil { + return err + } + + text = strings.TrimSpace(string(stdin)) + } + + if sdkTranslate.GetTargetLang() == "" { + return errors.New(l.Get("translate-target-required")) + } + + resp, err := sdkTranslate.SendRequest(text) + if err != nil { + return err + } + + fmt.Println(resp) + + return nil +} + +func TranslateInteractiveMode() error { + translateSdk := sdk.GetSdkTranslate() + l := lang.GetLocalize() + + for { + input := utils.Input(l.Get("translate-input"), "", false) + if input == "exit" { + break + } + + if translateSdk.GetTargetLang() == "" { + return errors.New(l.Get("translate-target-required")) + } + + resp, err := translateSdk.SendRequest(input) + if err != nil { + return err + } + + fmt.Print("\n") + fmt.Println(utils.Red + "> " + utils.Reset + resp) + fmt.Print("\n") + } + + return nil +}