diff --git a/examples/aiexamples/groq.vsh b/examples/aiexamples/groq.vsh index 860fe797..7ccd4db3 100755 --- a/examples/aiexamples/groq.vsh +++ b/examples/aiexamples/groq.vsh @@ -8,6 +8,13 @@ import os fn test1(mut client openai.OpenAI)!{ + + instruction:=' + You are a template language converter. You convert Pug templates to Jet templates. + + The target template language, Jet, is defined as follows: + ' + // Create a chat completion request res := client.chat_completion(msgs:openai.Messages{ messages: [ diff --git a/examples/aiexamples/jetconvertor.vsh b/examples/aiexamples/jetconvertor.vsh new file mode 100755 index 00000000..5d5e5578 --- /dev/null +++ b/examples/aiexamples/jetconvertor.vsh @@ -0,0 +1,5 @@ +#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run + +import freeflowuniverse.herolib.mcp.aitools + +aitools.convert_pug("/root/code/github/freeflowuniverse/herolauncher/pkg/herolauncher/web/templates/admin")! \ No newline at end of file diff --git a/lib/clients/openai/completions.v b/lib/clients/openai/completions.v index e6d44647..d74fdbc1 100644 --- a/lib/clients/openai/completions.v +++ b/lib/clients/openai/completions.v @@ -46,6 +46,8 @@ struct ChatMessagesRaw { mut: model string messages []MessageRaw + temperature f64 = 0.5 + max_completion_tokens int = 32000 } @[params] @@ -53,7 +55,8 @@ pub struct CompletionArgs{ pub mut: model string msgs Messages - + temperature f64 = 0.5 + max_completion_tokens int = 32000 } // creates a new chat completion given a list of messages @@ -65,6 +68,8 @@ pub fn (mut f OpenAI) chat_completion(args_ CompletionArgs) !ChatCompletion { } mut m := ChatMessagesRaw{ model: args.model + temperature: args.temperature + max_completion_tokens: args.max_completion_tokens } for msg in args.msgs.messages { mr := MessageRaw{ diff --git a/lib/mcp/aitools/convertpug.v b/lib/mcp/aitools/convertpug.v new file mode 100644 index 00000000..7696344d --- /dev/null +++ b/lib/mcp/aitools/convertpug.v @@ -0,0 +1,305 @@ +module aitools + +import freeflowuniverse.herolib.clients.openai +import freeflowuniverse.herolib.core.texttools +import freeflowuniverse.herolib.core.pathlib +import json + +pub fn convert_pug(mydir string)! { + + mut d:=pathlib.get_dir(path: mydir, create:false)! + list := d.list(regex:[r'.*\.pug$'],include_links:false,files_only:true)! + for item in list.paths{ + convert_pug_file(item.path)! + } + +} + +// extract_template parses AI response content to extract just the template +fn extract_template(raw_content string) string { + mut content := raw_content + + // First check for tag + if content.contains('') { + content = content.split('')[1].trim_space() + } + + // Look for ```jet code block + if content.contains('```jet') { + parts := content.split('```jet') + if parts.len > 1 { + end_parts := parts[1].split('```') + if end_parts.len > 0 { + content = end_parts[0].trim_space() + } + } + } else if content.contains('```') { + // If no ```jet, look for regular ``` code block + parts := content.split('```') + if parts.len >= 2 { + // Take the content between the first set of ``` + // This handles both ```content``` and cases where there's only an opening ``` + content = parts[1].trim_space() + + // If we only see an opening ``` but no closing, cleanup any remaining backticks + // to avoid incomplete formatting markers + if !content.contains('```') { + content = content.replace('`', '') + } + } + } + + return content +} + +pub fn convert_pug_file(myfile string)! { + println(myfile) + + mut content_path := pathlib.get_file(path: myfile, create: false)! + content := content_path.read()! + + mut l := loader() + mut client := openai.get()! + + base_instruction := ' + You are a template language converter. You convert Pug templates to Jet templates. + + The target template language, Jet, is defined as follows: + ' + + base_user_prompt := ' + Convert this following Pug template to Jet: + + only output the resulting template, no explanation, no steps, just the jet template + ' + + // Create new file path by replacing .pug extension with .jet + jet_file := myfile.replace('.pug', '.jet') + + // We'll retry up to 5 times if validation fails + max_attempts := 5 + mut attempts := 0 + mut is_valid := false + mut error_message := '' + mut template := '' + + for attempts < max_attempts && !is_valid { + attempts++ + + mut system_content := texttools.dedent(base_instruction) + "\n" + l.jet() + + // Generate the user prompt based on whether this is initial attempt or retry + mut user_prompt := '' + + if attempts == 1 { + // First attempt - use original pug content + user_prompt = texttools.dedent(base_user_prompt) + "\n" + content + println('First attempt: Converting from Pug to Jet') + } else { + // Retry - focus on fixing the template errors + println('Attempt ${attempts}: Retrying with error feedback') + user_prompt = ' + The previous Jet template conversion had the following error: + ERROR: ${error_message} + + Here was the template that had errors: + ``` + ${template} + ``` + + Please fix the template and try again. Return only the corrected Jet template.' + } +module aitools + +import freeflowuniverse.herolib.clients.openai +import freeflowuniverse.herolib.core.texttools +import freeflowuniverse.herolib.core.pathlib +import json + +pub fn convert_pug(mydir string)! { + + mut d:=pathlib.get_dir(path: mydir, create:false)! + list := d.list(regex:[r'.*\.pug$'],include_links:false,files_only:true)! + for item in list.paths{ + convert_pug_file(item.path)! + } + +} + +// extract_template parses AI response content to extract just the template +fn extract_template(raw_content string) string { + mut content := raw_content + + // First check for tag + if content.contains('') { + content = content.split('')[1].trim_space() + } + + // Look for ```jet code block + if content.contains('```jet') { + parts := content.split('```jet') + if parts.len > 1 { + end_parts := parts[1].split('```') + if end_parts.len > 0 { + content = end_parts[0].trim_space() + } + } + } else if content.contains('```') { + // If no ```jet, look for regular ``` code block + parts := content.split('```') + if parts.len >= 2 { + // Take the content between the first set of ``` + // This handles both ```content``` and cases where there's only an opening ``` + content = parts[1].trim_space() + + // If we only see an opening ``` but no closing, cleanup any remaining backticks + // to avoid incomplete formatting markers + if !content.contains('```') { + content = content.replace('`', '') + } + } + } + + return content +} + +pub fn convert_pug_file(myfile string)! { + println(myfile) + + mut content_path := pathlib.get_file(path: myfile, create: false)! + content := content_path.read()! + + mut l := loader() + mut client := openai.get()! + + base_instruction := ' + You are a template language converter. You convert Pug templates to Jet templates. + + The target template language, Jet, is defined as follows: + ' + + base_user_prompt := ' + Convert this following Pug template to Jet: + + only output the resulting template, no explanation, no steps, just the jet template + ' + + // Create new file path by replacing .pug extension with .jet + jet_file := myfile.replace('.pug', '.jet') + + // We'll retry up to 5 times if validation fails + max_attempts := 5 + mut attempts := 0 + mut is_valid := false + mut error_message := '' + mut template := '' + + for attempts < max_attempts && !is_valid { + attempts++ + +module aitools + +import freeflowuniverse.herolib.clients.openai +import freeflowuniverse.herolib.core.texttools +import freeflowuniverse.herolib.core.pathlib +import json + +pub fn convert_pug(mydir string)! { + + mut d:=pathlib.get_dir(path: mydir, create:false)! + list := d.list(regex:[r'.*\.pug$'],include_links:false,files_only:true)! + for item in list.paths{ + convert_pug_file(item.path)! + } + +} + +// extract_template parses AI response content to extract just the template +fn extract_template(raw_content string) string { + mut content := raw_content + + // First check for tag + if content.contains('') { + content = content.split('')[1].trim_space() + } + + // Look for ```jet code block + if content.contains('```jet') { + parts := content.split('```jet') + if parts.len > 1 { + end_parts := parts[1].split('```') + if end_parts.len > 0 { + content = end_parts[0].trim_space() + } + } + } else if content.contains('```') { + // If no ```jet, look for regular ``` code block + parts := content.split('```') + if parts.len >= 2 { + // Take the content between the first set of ``` + // This handles both ```content``` and cases where there's only an opening ``` + content = parts[1].trim_space() + + // If we only see an opening ``` but no closing, cleanup any remaining backticks + // to avoid incomplete formatting markers + if !content.contains('```') { + content = content.replace('`', '') + } + } + } + + return content +} + +pub fn convert_pug_file(myfile string)! { + println(myfile) + + mut content_path := pathlib.get_file(path: myfile, create: false)! + content := content_path.read()! + + mut l := loader() + mut client := openai.get()! + + base_instruction := ' + You are a template language converter. You convert Pug templates to Jet templates. + + The target template language, Jet, is defined as follows: + ' + + base_user_prompt := ' + Convert this following Pug template to Jet: + + only output the resulting template, no explanation, no steps, just the jet template + ' + + // Create new file path by replacing .pug extension with .jet + jet_file := myfile.replace('.pug', '.jet') + + // We'll retry up to 5 times if validation fails + max_attempts := 5 + mut attempts := 0 + mut is_valid := false + mut error_message := '' + mut template := '' + + for attempts < max_attempts && !is_valid { + attempts++ + + mut system_content := texttools.dedent(base_instruction) + "\n" + l.jet() + mut user_prompt := texttools.dedent(base_user_prompt) + "\n" + content + + // If this is a retry, add the error information to the prompt + if attempts > 1 { + println('Attempt ${attempts}: Retrying with error feedback') + user_prompt = ' + The previous template conversion had the following error: + ERROR: ${error_message} + + Here was the template that had errors: + ``` + ${template} + ``` + + Please fix the template and try again. Return only the corrected template. + ' + user_prompt + } diff --git a/lib/mcp/aitools/jetvalidation.v b/lib/mcp/aitools/jetvalidation.v new file mode 100644 index 00000000..1258c147 --- /dev/null +++ b/lib/mcp/aitools/jetvalidation.v @@ -0,0 +1,85 @@ +module aitools + +import freeflowuniverse.herolib.core.httpconnection +import json + +// JetTemplateResponse is the expected response structure from the validation service +struct JetTemplateResponse { + valid bool + message string + error string +} + +// ValidationResult represents the result of a template validation +pub struct ValidationResult { +pub: + is_valid bool + error string +} + +// jetvaliditycheck validates a Jet template by sending it to a validation service +// The function sends the template to http://localhost:9020/checkjet for validation +// Returns a ValidationResult containing validity status and any error messages +pub fn jetvaliditycheck(jetcontent string) !ValidationResult { + // Create HTTP connection to the validation service + mut conn := httpconnection.HTTPConnection{ + base_url: 'http://localhost:9020' + } + + // Prepare the request data - template content wrapped in JSON + template_data := json.encode({ + 'template': jetcontent + }) + + // Print what we're sending to the AI service + // println('Sending to JET validation service:') + // println('--------------------------------') + // println(jetcontent) + // println('--------------------------------') + + // Send the POST request to the validation endpoint + req := httpconnection.Request{ + prefix: 'checkjet', + data: template_data, + dataformat: .json + } + + // Execute the request + result := conn.post_json_str(req) or { + // Handle connection errors + return ValidationResult{ + is_valid: false + error: 'Connection error: ${err}' + } + } + + // Attempt to parse the response as JSON using the expected struct + response := json.decode(JetTemplateResponse, result) or { + // If we can't parse JSON using our struct, the server didn't return the expected format + return ValidationResult{ + is_valid: false + error: 'Server returned unexpected format: ${err.msg()}' + } + } + + // Use the structured response data + if response.valid == false{ + error_msg := if response.error != '' { + response.error + } else if response.message != '' { + response.message + } else { + 'Unknown validation error' + } + + return ValidationResult{ + is_valid: false + error: error_msg + } + } + + return ValidationResult{ + is_valid: true + error: '' + } +} diff --git a/lib/mcp/aitools/loader.v b/lib/mcp/aitools/loader.v new file mode 100644 index 00000000..278207af --- /dev/null +++ b/lib/mcp/aitools/loader.v @@ -0,0 +1,26 @@ +module aitools + +import v.embed_file +import os + +@[heap] +pub struct FileLoader { +pub mut: + embedded_files map[string]embed_file.EmbedFileData @[skip; str: skip] +} + +fn (mut loader FileLoader) load() { + loader.embedded_files["jet"]=$embed_file('templates/jet_instructions.md') +} + + +fn (mut loader FileLoader) jet() string { + c:=loader.embedded_files["jet"] or { panic("bug embed") } + return c.to_string() +} + +fn loader() FileLoader { + mut loader := FileLoader{} + loader.load() + return loader +} \ No newline at end of file diff --git a/lib/mcp/aitools/templates/jet_instructions.md b/lib/mcp/aitools/templates/jet_instructions.md new file mode 100644 index 00000000..5bf8cee7 --- /dev/null +++ b/lib/mcp/aitools/templates/jet_instructions.md @@ -0,0 +1,446 @@ +# Jet Template Engine Syntax Reference + +## Delimiters + +Template delimiters are `{{` and `}}`. +Delimiters can use `.` to output the execution context: + +```jet +hello {{ . }} +``` + +### Whitespace Trimming + +Whitespace around delimiters can be trimmed using `{{-` and `-}}`: + +```jet +foo {{- "bar" -}} baz +``` + +Whitespace includes spaces, tabs, carriage returns, and newlines. + +### Comments + +Comments use `{* ... *}`: + +```jet +{* this is a comment *} + +{* + Multiline + {{ expressions }} are ignored +*} +``` + +--- + +## Variables + +### Initialization + +```jet +{{ foo := "bar" }} +``` + +### Assignment + +```jet +{{ foo = "asd" }} +{{ foo = 4711 }} +``` + +Skip assignment but still evaluate: + +```jet +{{ _ := stillRuns() }} +{{ _ = stillRuns() }} +``` + +--- + +## Expressions + +### Identifiers + +Identifiers resolve to values: + +```jet +{{ len("hello") }} +{{ isset(foo, bar) }} +``` + +### Indexing + +#### String + +```jet +{{ s := "helloworld" }} +{{ s[1] }} +``` + +#### Slice / Array + +```jet +{{ s := slice("foo", "bar", "asd") }} +{{ s[0] }} +{{ s[2] }} +``` + +#### Map + +```jet +{{ m := map("foo", 123, "bar", 456) }} +{{ m["foo"] }} +``` + +#### Struct + +```jet +{{ user["Name"] }} +``` + +### Field Access + +#### Map + +```jet +{{ m.foo }} +{{ range s }} + {{ .foo }} +{{ end }} +``` + +#### Struct + +```jet +{{ user.Name }} +{{ range users }} + {{ .Name }} +{{ end }} +``` + +### Slicing + +```jet +{{ s := slice(6, 7, 8, 9, 10, 11) }} +{{ sevenEightNine := s[1:4] }} +``` + +### Arithmetic + +```jet +{{ 1 + 2 * 3 - 4 }} +{{ (1 + 2) * 3 - 4.1 }} +``` + +### String Concatenation + +```jet +{{ "HELLO" + " " + "WORLD!" }} +``` + +#### Logical Operators + +- `&&` +- `||` +- `!` +- `==`, `!=` +- `<`, `>`, `<=`, `>=` + +```jet +{{ item == true || !item2 && item3 != "test" }} +{{ item >= 12.5 || item < 6 }} +``` + +### Ternary Operator + +```jet +