From eeb671bb9da1c50c0de399d48bbe283db57c58c6 Mon Sep 17 00:00:00 2001 From: nash Date: Sun, 15 Sep 2024 13:41:18 +0800 Subject: [PATCH 1/5] OpenAI and Ollama Clients --- modules/ai/README.md | 11 +++ modules/ai/mod.nu | 24 ++++++ modules/ai/ollama.nu | 186 +++++++++++++++++++++++++++++++++++++++++++ modules/ai/openai.nu | 124 +++++++++++++++++++++++++++++ modules/ai/prompt.nu | 25 ++++++ 5 files changed, 370 insertions(+) create mode 100644 modules/ai/README.md create mode 100644 modules/ai/mod.nu create mode 100644 modules/ai/ollama.nu create mode 100644 modules/ai/openai.nu create mode 100644 modules/ai/prompt.nu diff --git a/modules/ai/README.md b/modules/ai/README.md new file mode 100644 index 00000000..5d0d8180 --- /dev/null +++ b/modules/ai/README.md @@ -0,0 +1,11 @@ +OpenAI and Ollama Clients + +- Streaming output +- The OpenAI interface employs the `ai` prefix for user-friendly input. +- Option for controllable return values +- Supports chat context retention +- Customizable prompt functionality for `ai do` + - Refer to [prompt.nu](prompt.nu) for definition guidelines + - Default model can be overridden using `--model` +- Importing and exporting of Ollama models +- Connection details managed through environment variables diff --git a/modules/ai/mod.nu b/modules/ai/mod.nu new file mode 100644 index 00000000..dbf13703 --- /dev/null +++ b/modules/ai/mod.nu @@ -0,0 +1,24 @@ +export-env { + use ollama.nu * + use openai.nu * + use prompt.nu * +} + +export use ollama.nu * +export use openai.nu * + + +export def 'similarity cosine' [a b] { + if ($a | length) != ($b | length) { + print "The lengths of the vectors must be equal." + } + $a | zip $b | reduce -f {p: 0, a: 0, b: 0} {|i,a| + { + p: ($a.p + ($i.0 * $i.1)) + a: ($a.a + ($i.0 * $i.0)) + b: ($a.b + ($i.1 * $i.1)) + } + } + | $in.p / (($in.a | math sqrt) * ($in.b | math sqrt)) +} + diff --git a/modules/ai/ollama.nu b/modules/ai/ollama.nu new file mode 100644 index 00000000..7e75c294 --- /dev/null +++ b/modules/ai/ollama.nu @@ -0,0 +1,186 @@ +export-env { + $env.OLLAMA_HOST = "http://localhost:11434" + $env.OLLAMA_CHAT = {} + $env.OLLAMA_HOME = [$env.HOME .ollama] | path join +} + +def "nu-complete models" [] { + http get $"($env.OLLAMA_HOST)/api/tags" + | get models + | each {{value: $in.name, description: $in.modified_at}} +} + +export def "ollama info" [model: string@"nu-complete models"] { + http post -t application/json $"($env.OLLAMA_HOST)/api/show" {name: $model} +} + +export def "ollama embed" [ + model: string@"nu-complete models" + input: string +] { + http post -t application/json $"($env.OLLAMA_HOST)/api/embed" { + model: $model, input: [$input] + } + | get embeddings.0 +} + + +export def "ollama gen" [ + model: string@"nu-complete models" + prompt: string + --image(-i): path + --full(-f) +] { + let content = $in | default "" + let img = if ($image | is-empty) { + {} + } else { + {images: [(open $image | encode base64)]} + } + let r = http post -t application/json $"($env.OLLAMA_HOST)/api/generate" { + model: $model + prompt: ($prompt | str replace "{}" $content) + stream: false + ...$img + } + if $full { + $r + } else { + $r.response + } +} + + +export def --env "ollama chat" [ + model: string@"nu-complete models" + message: string + --image(-i): path + --reset(-r) + --forget(-f) + --placehold(-p): string = '{}' + --out(-o) + --debug +] { + let content = $in | default "" + let img = if ($image | is-empty) { + {} + } else { + {images: [(open $image | encode base64)]} + } + let msg = { + role: "user" + content: ($message | str replace -m $placehold $content) + ...$img + } + if $debug { + print $"(ansi grey)($msg.content)(ansi reset)" + } + if not $forget { + if ($env.OLLAMA_CHAT | is-empty) or ($model not-in $env.OLLAMA_CHAT) { + $env.OLLAMA_CHAT = ($env.OLLAMA_CHAT | insert $model []) + } + if $reset { + $env.OLLAMA_CHAT = ($env.OLLAMA_CHAT | update $model []) + print '✨' + } + $env.OLLAMA_CHAT = ($env.OLLAMA_CHAT | update $model {|x| $x | get $model | append $msg}) + } + + let r = http post -t application/json $"($env.OLLAMA_HOST)/api/chat" { + model: $model + messages: [ + ...(if $forget { [] } else { $env.OLLAMA_CHAT | get $model }) + $msg + ] + stream: true + } + | lines + | reduce -f {msg: '', token: 0} {|i,a| + let x = $i | parse -r '.*?(?\{.*)' + if ($x | is-empty) { return $a } + let x = $x | get 0.data | from json + let m = $x.message.content + print -n $m + $a + | update msg {|x| $x.msg + $m } + | update token {|x| $x.token + 1 } + } + if not $forget { + let r = {role: 'assistant', content: $r.msg, token: $r.token} + $env.OLLAMA_CHAT = ($env.OLLAMA_CHAT | update $model {|x| $x | get $model | append $r }) + } + if $out { $r.msg } +} + + + + +def "nu-complete ollama model" [] { + cd $"($env.OLLAMA_HOME)/models/manifests/" + ls **/* | where type == file | get name +} + +export def "ollama export" [ + model: string@"nu-complete ollama model" + target + --home: string +] { + if ($target | path exists) { + if ([y n] | input list "already exists, remove it?") == 'y' { + rm -rf $target + } else { + return + } + } + mkdir $target + + let base = { + blob: ([$env.OLLAMA_HOME models blobs] | path join) + manifests: ([$env.OLLAMA_HOME models manifests] | path join) + } + + let tg = { + bin: ([$target model.bin] | path join) + model: ([$target Modelfile] | path join) + source: ([$target source.txt] | path join) + } + + $model | split row '/' | $"($in | range 0..<-1 | str join '/'):($in | last)" | save $tg.source + + + let manifests = open ([$base.manifests $model] | path join) | from json + + for i in $manifests.layers { + + let digest = $i.digest + let type = $i.mediaType | split row '.' | last + let blob = [$base.blob ($i.digest | str replace ':' '-')] | path join + match $type { + model => { + cp $blob $tg.bin + $"FROM ./model.bin(char newline)" | save -a $tg.model + } + params => { + let p = open $blob | from json + $p + | items {|k,v| {k: $k, v: $v} } + | each {|x| $x.v | each {|y| $'PARAMETER ($x.k) "($y)"' } } + | flatten + | str join (char newline) + | $"(char newline)($in)" + | save -a $tg.model + } + _ => { + $'(char newline)($type | str upcase) """(cat $blob)"""' | save -a $tg.model + } + } + } + + print 'success' +} + +export def "ollama import" [dir] { + cd $dir + let model = cat source.txt + ollama create $model +} diff --git a/modules/ai/openai.nu b/modules/ai/openai.nu new file mode 100644 index 00000000..8f891d02 --- /dev/null +++ b/modules/ai/openai.nu @@ -0,0 +1,124 @@ +export-env { + $env.OPENAI_HOST = "http://localhost:11434" + $env.OPENAI_CHAT = {} + $env.OPENAI_API_KEY = 'secret' + $env.OPENAI_ORG_ID = '' + $env.OPENAI_PROJECT_ID = '' +} + + +def "nu-complete models" [] { + http get --headers [ + Authorization $"Bearer ($env.OPENAI_API_KEY)" + OpenAI-Organization $env.OPENAI_ORG_ID + OpenAI-Project $env.OPENAI_PROJECT_ID + ] $"($env.OPENAI_HOST)/v1/models" + | get data.id +} + + +export def --env "ai chat" [ + model: string@"nu-complete models" + message: string + --image(-i): path + --reset(-r) + --forget(-f) + --placehold(-p): string = '{}' + --out(-o) + --debug +] { + let content = $in | default "" + let img = if ($image | is-empty) { + {} + } else { + {images: [(open $image | encode base64)]} + } + let msg = { + role: "user" + content: ($message | str replace -m $placehold $content) + ...$img + } + if $debug { + print $"(ansi grey)($message)\n---\n($placehold)\n---(ansi reset)" + print $"(ansi grey)($msg.content)\n---(ansi reset)" + } + if not $forget { + if ($env.OPENAI_CHAT | is-empty) or ($model not-in $env.OPENAI_CHAT) { + $env.OPENAI_CHAT = ($env.OPENAI_CHAT | insert $model []) + } + if $reset { + $env.OPENAI_CHAT = ($env.OPENAI_CHAT | update $model []) + print '✨' + } + $env.OPENAI_CHAT = ($env.OPENAI_CHAT | update $model {|x| $x | get $model | append $msg}) + } + + let r = http post -t application/json --headers [ + Authorization $"Bearer ($env.OPENAI_API_KEY)" + ] $"($env.OPENAI_HOST)/v1/chat/completions" { + model: $model + messages: [ + ...(if $forget { [] } else { $env.OPENAI_CHAT | get $model }) + $msg + ] + stream: true + } + | lines + | reduce -f {msg: '', token: 0} {|i,a| + let x = $i | parse -r '.*?(?\{.*)' + if ($x | is-empty) { return $a } + let x = $x | get 0.data | from json + let m = $x.choices | each { $in.delta.content } | str join + print -n $m + $a + | update msg {|x| $x.msg + $m } + | update token {|x| $x.token + 1 } + } + if not $forget { + let r = {role: 'assistant', content: $r.msg, token: $r.token} + $env.OPENAI_CHAT = ($env.OPENAI_CHAT | update $model {|x| $x | get $model | append $r }) + } + if $out { $r.msg } +} + + +export def "ai embed" [ + model: string@"nu-complete models" + input: string +] { + http post -t application/json $"($env.OPENAI_HOST)/v1/embeddings" { + model: $model, input: [$input], encoding_format: 'float' + } + | get data.0.embedding +} + + +def 'nu-complete role' [ctx] { + $env.OPENAI_PROMPT | items {|k, v| {value: $k, description: $v.description? } } +} + +export def 'ai do' [ + role: string@"nu-complete role" + input?: string + --out(-o) + --model(-m): string@"nu-complete models" + --debug +] { + let input = if ($in | is-empty) { $input } else { $in } + let placehold = $"<(random chars -l 6)>" + let role = $env.OPENAI_PROMPT | get $role + let model = if ($model | is-empty) { + $role | get model + } else { + $model + } + let prompt = $role | get prompt | each {|x| + if ($x | str replace -ar "['\"`]+" '' | $in == '{}') { + $x | str replace '{}' $placehold + } else { + $x + } + } | str join (char newline) + + $input | ai chat $model -p $placehold --out=$out --debug=$debug $prompt +} diff --git a/modules/ai/prompt.nu b/modules/ai/prompt.nu new file mode 100644 index 00000000..79463417 --- /dev/null +++ b/modules/ai/prompt.nu @@ -0,0 +1,25 @@ +export-env { + $env.OPENAI_PROMPT = { + 'json-to-sql': { + prompt: [ + "Analyze the following JSON data to convert it into a SQL statement for creating a table:" + "```" + "{}" + "```" + ], + model: 'qwen2:1.5b', + description: 'Analyze JSON content, converting it into a SQL create table statement' + }, + 'trans-to-en': { + prompt: [ + "Translate the following text into English:" + "```" + "{}" + "```" + ], + model: 'qwen2:1.5b', + description: 'Translation to English' + } + + } +} From 355d39e87aa7e4463f94ecafd1b35b5bd388356d Mon Sep 17 00:00:00 2001 From: nash Date: Sun, 15 Sep 2024 13:56:47 +0800 Subject: [PATCH 2/5] Add some custom examples --- modules/ai/README.md | 16 ++++++++++++++++ modules/ai/prompt.nu | 11 +++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/modules/ai/README.md b/modules/ai/README.md index 5d0d8180..108875b9 100644 --- a/modules/ai/README.md +++ b/modules/ai/README.md @@ -9,3 +9,19 @@ OpenAI and Ollama Clients - Default model can be overridden using `--model` - Importing and exporting of Ollama models - Connection details managed through environment variables + +Control some options with the following code. +``` +$env.OLLAMA_HOST = 'http://localhost:11434' +$env.OPENAI_HOST = 'http://localhost:11434' +$env.OPENAI_API_KEY = 'secret' +$env.OPENAI_PROMPT = $env.OPENAI_PROMPT +| insert 'json2rust' { + prompt: [ + "Analyze the following JSON data to convert it into a Rust struct:" + "```{}```" + ] + model: '', + description: 'Analyze JSON content, converting it into a Rust struct' +} +``` diff --git a/modules/ai/prompt.nu b/modules/ai/prompt.nu index 79463417..525e004c 100644 --- a/modules/ai/prompt.nu +++ b/modules/ai/prompt.nu @@ -1,5 +1,13 @@ export-env { $env.OPENAI_PROMPT = { + 'json-to-jsonschema': { + prompt: [ + "Analyze the following JSON data to convert it into a jsonschema:" + "```{}```" + ] + model: '', + description: 'Analyze JSON content, converting it into a jsonschema' + } 'json-to-sql': { prompt: [ "Analyze the following JSON data to convert it into a SQL statement for creating a table:" @@ -9,7 +17,7 @@ export-env { ], model: 'qwen2:1.5b', description: 'Analyze JSON content, converting it into a SQL create table statement' - }, + } 'trans-to-en': { prompt: [ "Translate the following text into English:" @@ -20,6 +28,5 @@ export-env { model: 'qwen2:1.5b', description: 'Translation to English' } - } } From 005df9bb43890870d56cac4ae46ffb65f4cd9bdb Mon Sep 17 00:00:00 2001 From: nash Date: Sun, 15 Sep 2024 14:08:54 +0800 Subject: [PATCH 3/5] Explanation about placeholders --- modules/ai/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/ai/README.md b/modules/ai/README.md index 108875b9..cb1a2e99 100644 --- a/modules/ai/README.md +++ b/modules/ai/README.md @@ -7,6 +7,7 @@ OpenAI and Ollama Clients - Customizable prompt functionality for `ai do` - Refer to [prompt.nu](prompt.nu) for definition guidelines - Default model can be overridden using `--model` + - line containing placeholders in the prompt can only include `{}` and quotation marks - Importing and exporting of Ollama models - Connection details managed through environment variables From 10ce48df9909f84e75d3e7760290865e75047359 Mon Sep 17 00:00:00 2001 From: nash Date: Sun, 15 Sep 2024 16:45:37 +0800 Subject: [PATCH 4/5] git-diff-summary The commit adds a new feature to the AI prompt module. It introduces a 'git-diff-summary' functi on which is designed to extract commit logs from git differences. This feature focuses on summar izing only the content changes in files and ignores hash changes. The prompt template for this f unction is included within the commit, showing how the summary should be structured. Additionall y, a description is provided for the new functionality, explaining its purpose as a tool to summ arize git differences. --- modules/ai/prompt.nu | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/modules/ai/prompt.nu b/modules/ai/prompt.nu index 525e004c..f04e3e7b 100644 --- a/modules/ai/prompt.nu +++ b/modules/ai/prompt.nu @@ -18,6 +18,15 @@ export-env { model: 'qwen2:1.5b', description: 'Analyze JSON content, converting it into a SQL create table statement' } + 'git-diff-summary': { + prompt: [ + "Extract commit logs from git differences, summarizing only the content changes in files while ignoring hash changes." + "```" + "{}" + "```" + ] + description: 'Summarize from git differences' + } 'trans-to-en': { prompt: [ "Translate the following text into English:" From c9b57d4eb417e1f8314c97502debaf4d3ec9963a Mon Sep 17 00:00:00 2001 From: nash Date: Sun, 15 Sep 2024 18:14:47 +0800 Subject: [PATCH 5/5] Allow an arbitrary number of placeholder tokens in prompt templates. --- modules/ai/openai.nu | 25 ++++++++++++++++++++----- modules/ai/prompt.nu | 16 ++++++++++++++++ 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/modules/ai/openai.nu b/modules/ai/openai.nu index 8f891d02..b03dae45 100644 --- a/modules/ai/openai.nu +++ b/modules/ai/openai.nu @@ -94,19 +94,30 @@ export def "ai embed" [ def 'nu-complete role' [ctx] { - $env.OPENAI_PROMPT | items {|k, v| {value: $k, description: $v.description? } } + let args = $ctx | split row '|' | last | str trim -l | split row ' ' | range 2.. + let len = $args | length + match $len { + 1 => { + $env.OPENAI_PROMPT | items {|k, v| {value: $k, description: $v.description? } } + } + _ => { + let role = $env.OPENAI_PROMPT | get $args.0 + let ph = $role.placeholder? | get ($len - 2) + $ph | columns + } + } } export def 'ai do' [ - role: string@"nu-complete role" - input?: string + ...args: string@"nu-complete role" --out(-o) --model(-m): string@"nu-complete models" --debug ] { - let input = if ($in | is-empty) { $input } else { $in } + let input = if ($in | is-empty) { $args | last } else { $in } + let argv = if ($in | is-empty) { $args | range 1..<-1 } else { $args | range 1.. } + let role = $env.OPENAI_PROMPT | get $args.0 let placehold = $"<(random chars -l 6)>" - let role = $env.OPENAI_PROMPT | get $role let model = if ($model | is-empty) { $role | get model } else { @@ -119,6 +130,10 @@ export def 'ai do' [ $x } } | str join (char newline) + let prompt = $argv | enumerate + | reduce -f $prompt {|i,a| + $a | str replace '{}' (($role.placeholder? | get $i.index) | get $i.item) + } $input | ai chat $model -p $placehold --out=$out --debug=$debug $prompt } diff --git a/modules/ai/prompt.nu b/modules/ai/prompt.nu index f04e3e7b..7b4c825f 100644 --- a/modules/ai/prompt.nu +++ b/modules/ai/prompt.nu @@ -1,4 +1,10 @@ export-env { + let expert = { + rust: 'You are a Rust language expert.' + js: 'You are a Javascript language expert.' + python: 'You are a Python language expert.' + nushell: 'You are a Nushell language expert.' + } $env.OPENAI_PROMPT = { 'json-to-jsonschema': { prompt: [ @@ -27,6 +33,16 @@ export-env { ] description: 'Summarize from git differences' } + 'debug': { + prompt: [ + "{} Analyze the causes of the error and provide suggestions for correction." + "```" + "{}" + "```" + ] + placeholder: [ $expert ] + description: 'Programming language experts help you debug.' + } 'trans-to-en': { prompt: [ "Translate the following text into English:"