diff --git a/Project.toml b/Project.toml index edb60c4..532151e 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ConstraintsTranslator" uuid = "314c63f5-3dda-4b35-95e7-4cc933f13053" authors = ["Jean-François BAFFIER (@Azzaare)"] -version = "0.0.2" +version = "0.0.3" [deps] Constraints = "30f324ab-b02d-43f0-b619-e131c61659f7" diff --git a/README.md b/README.md index a76fe3a..7b9e959 100644 --- a/README.md +++ b/README.md @@ -37,19 +37,14 @@ Finally, we can start playing with the package. Below, an example for translatin ```julia using ConstraintsTranslator -llm = GoogleLLM("gemini-1.5-pro") +llm = GoogleLLM("gemini-1.5-pro-latest") description = """ We need to determine the shortest possible route for a salesman who must visit a set of cities exactly once and return to the starting city. The objective is to minimize the total travel distance while ensuring that each city is visited exactly once. Example input data: -1. cities.csv -city_id,city_name -1,CityA -2,CityB - -2. distances.csv +1. distances.csv from,to,distance CityA,CityB,10 CityA,CityC,8 diff --git a/src/ConstraintsTranslator.jl b/src/ConstraintsTranslator.jl index 126ff31..8be1216 100644 --- a/src/ConstraintsTranslator.jl +++ b/src/ConstraintsTranslator.jl @@ -4,6 +4,7 @@ module ConstraintsTranslator import Constraints: USUAL_CONSTRAINTS import HTTP import InteractiveUtils +import InteractiveUtils: clipboard import JSONSchema import JSON3 import REPL diff --git a/src/llm.jl b/src/llm.jl index 1fe86f7..df30509 100644 --- a/src/llm.jl +++ b/src/llm.jl @@ -6,7 +6,9 @@ abstract type OpenAILLM <: AbstractLLM end """ GroqLLM + Structure encapsulating the parameters for accessing the Groq LLM API. + - `api_key`: an API key for accessing the Groq API (https://groq.com), read from the environmental variable GROQ_API_KEY. - `model_id`: a string identifier for the model to query. See https://console.groq.com/docs/models for the list of available models. - `url`: URL for chat completions. Defaults to "https://api.groq.com/openai/v1/chat/completions". @@ -16,7 +18,7 @@ struct GroqLLM <: OpenAILLM model_id::String url::String - function GroqLLM(model_id::String = "llama3-70b-8192", url = GROQ_URL) + function GroqLLM(model_id::String = "llama-3.1-70b-versatile", url = GROQ_URL) api_key = get(ENV, "GROQ_API_KEY", "") if isempty(api_key) error("Environment variable GROQ_API_KEY is not set") @@ -27,17 +29,19 @@ end """ Google LLM + Structure encapsulating the parameters for accessing the Google LLM API. -- `api_key`: an API key for accessing the Google Gemini API (https://ai.google.dev/gemini-api/docs/), read from the environmental variable GOOGLE_API_KEY. -- `model_id`: a string identifier for the model to query. See https://ai.google.dev/gemini-api/docs/models/gemini for the list of available models. -- `url`: URL for chat completions. Defaults to ""https://generativelanguage.googleapis.com/v1beta/models/{{model_id}}". + +- `api_key`: an API key for accessing the Google Gemini API (`https://ai.google.dev/gemini-api/docs/`), read from the environmental variable `GOOGLE_API_KEY`. +- `model_id`: a string identifier for the model to query. See `https://ai.google.dev/gemini-api/docs/models/gemini` for the list of available models. +- `url`: URL for chat completions. Defaults to `https://generativelanguage.googleapis.com/v1beta/models/{{model_id}}`. """ struct GoogleLLM <: AbstractLLM api_key::String model_id::String url::String - function GoogleLLM(model_id::String = "gemini-1.5-flash") + function GoogleLLM(model_id::String = "gemini-1.5-flash-latest") api_key = get(ENV, "GOOGLE_API_KEY", "") if isempty(api_key) error("Environment variable GOOGLE_API_KEY is not set") @@ -48,12 +52,14 @@ end """ LlamaCppLLM + Structure encapsulating the parameters for accessing the llama.cpp server API. + - `api_key`: an optional API key for accessing the server - `model_id`: a string identifier for the model to query. Unused, kept for API compatibility. - `url`: the URL of the llama.cpp server OpenAI API endpoint (e.g., http://localhost:8080) -NOTE: we do not apply the appropriate chat templates to the prompt. -This must be handled either in an external code path or by the server. + +NOTE: we do not apply the appropriate chat templates to the prompt. This must be handled either in an external code path or by the server. """ struct LlamaCppLLM <: OpenAILLM api_key::String @@ -68,6 +74,7 @@ end """ get_completion(llm::OpenAILLM, prompt::Prompt) + Returns a completion for the given prompt using an OpenAI API compatible LLM """ function get_completion(llm::OpenAILLM, prompt::Prompt) @@ -89,6 +96,7 @@ end """ get_completion(llm::GoogleLLM, prompt::Prompt) + Returns a completion for the given prompt using the Google Gemini LLM API. """ function get_completion(llm::GoogleLLM, prompt::Prompt) @@ -110,6 +118,7 @@ end """ stream_completion(llm::OpenAILLM, prompt::Prompt) + Returns a completion for the given prompt using an OpenAI API compatible model. The completion is streamed to the terminal as it is generated. """ @@ -166,6 +175,7 @@ end """ stream_completion(llm::GoogleLLM, prompt::Prompt) + Returns a completion for the given prompt using the Google Gemini LLM API. The completion is streamed to the terminal as it is generated. """ @@ -192,6 +202,7 @@ function stream_completion(llm::GoogleLLM, prompt::Prompt) chunk = String(readavailable(io)) for line in eachmatch(r"(?<=data: ).*", chunk) if isnothing(line) + print("\n") continue end message = JSON3.read(line.match) @@ -206,6 +217,7 @@ end """ stream_completion(llm::AbstractLLM, prompt::AbstractPrompt) + Returns a completion for a `prompt` using the `llm` model API. The completion is streamed to the terminal as it is generated. """ @@ -216,8 +228,9 @@ end """ get_completion(llm::AbstractLLM, prompt::AbstractPrompt) + Returns a completion for a `prompt` using the `llm` model API. """ -function get_completion(llm::AbstractLLM, prompt::AbstractPrompt) - error("Not implemented for this LLM and/or prompt type.") +function get_completion(::AbstractLLM, ::AbstractPrompt) + return error("Not implemented for this LLM and/or prompt type.") end diff --git a/src/parsing.jl b/src/parsing.jl index 24ddd68..dd998fe 100644 --- a/src/parsing.jl +++ b/src/parsing.jl @@ -1,5 +1,6 @@ """ parse_code(s::String) + Parse the code blocks in the input string `s` delimited by triple backticks and an optional language annotation. Returns a dictionary keyed by language. Code blocks from the same language are concatenated. """ @@ -32,7 +33,8 @@ end """ check_syntax_errors(s::String) -Parses the string `s` as Julia code. In the case of syntax errors, it returns the error + +Parses the string `s` as Julia code. In the case of syntax errors, it returns the error message of the parser as a string. Otherwise, it returns an empty string. """ function check_syntax_errors(s::String) @@ -47,6 +49,7 @@ end """ edit_in_vim(s::String) + Edits the input string `s` in a temporary file using the Vim editor. Returns the modified string after the editor is closed. """ diff --git a/src/prompt.jl b/src/prompt.jl index 1357381..7feabc3 100644 --- a/src/prompt.jl +++ b/src/prompt.jl @@ -2,6 +2,7 @@ abstract type AbstractPrompt end """ Prompt + Simple data structure encapsulating a system prompt and a user prompt for LLM generation. ## Fields diff --git a/src/template.jl b/src/template.jl index 595f9c6..c7531e6 100644 --- a/src/template.jl +++ b/src/template.jl @@ -19,6 +19,8 @@ struct MetadataMessage <: AbstractMessage end """ + SystemMessage + Represents the prompt template of a system message. The template can optionally contain string placeholders enclosed in double curly braces, e.g., `{{variable}}`. Placeholders must be replaced with actual values when generating prompts. @@ -33,6 +35,8 @@ struct SystemMessage <: AbstractMessage end """ + UserMessage + Represents the prompt template of a user message. The template can optionally contain string placeholders enclosed in double curly braces, e.g., `{{variable}}`. Placeholders must be replaced with actual values when generating prompts. @@ -47,6 +51,8 @@ struct UserMessage <: AbstractMessage end """ + PromptTemplate + Represents a complete prompt template, comprising metadata, system, and user messages. # Fields @@ -63,7 +69,7 @@ end """ read_template(data_path::String) -Reads a prompt template from a JSON file specified by `data_path`. +Reads a prompt template from a JSON file specified by `data_path`. The function parses the JSON data and constructs a `PromptTemplate` object containing metadata, system, and user messages. TODO: validate the JSON data against a schema to ensure it is valid before parsing. @@ -165,4 +171,4 @@ function format_template(template::PromptTemplate; kwargs...) end return Prompt(system, user) -end \ No newline at end of file +end diff --git a/src/translate.jl b/src/translate.jl index b7feaef..9a9e03b 100644 --- a/src/translate.jl +++ b/src/translate.jl @@ -2,7 +2,8 @@ const MAX_RETRIES::Int = 3 """ extract_structure(model <: AbstractLLM, description <: AbstractString) -Extracts the parameters, decision variables and constraints of an optimization problem + +Extracts the parameters, decision variables and constraints of an optimization problem given a natural-language `description`. Returns a Markdown-formatted text containing the above information. """ @@ -22,6 +23,7 @@ function extract_structure( if interactive options = [ "Accept the response", + "Copy to clipboard", "Edit the response", "Try again with a different prompt", "Try again with the same prompt", @@ -33,13 +35,16 @@ function extract_structure( if choice == 1 break elseif choice == 2 + clipboard(response) + println("Response copied to the system's clipboard!") + elseif choice == 3 response = edit_in_editor(response) println(response) - elseif choice == 3 + elseif choice == 4 description = edit_in_editor(description) prompt = format_template(prompt_template; description, constraints) response = stream_completion(model, prompt) - elseif choice == 4 + elseif choice == 5 response = stream_completion(model, prompt) elseif choice == -1 InterruptException() @@ -51,8 +56,9 @@ end """ jumpify_model(model::AbstractLLM, description::AbstractString, examples::AbstractString) + Translates the natural language `description` of an optimization problem into a JuMP constraints -programming model to be solved with CBL by querying the Large Language Model `model`. +programming model to be solved with CBL by querying the Large Language Model `model`. The `examples` are snippets from `ConstraintModels.jl` used as in-context examples to the LLM. To work optimally, the model expects the `description` to be a structured Markdown-formatted description as the ones generated by `extract_structure`. @@ -77,6 +83,7 @@ function jumpify_model( options = [ "Accept the response", + "Copy to clipboard", "Edit the response", "Try again with a different prompt", "Try again with the same prompt", @@ -85,21 +92,24 @@ function jumpify_model( @warn "The generated Julia code has one or more syntax errors!" push!(options, "Fix syntax errors") end - menu = RadioMenu(options; pagesize = 5) + menu = RadioMenu(options; pagesize = 6) choice = request("What do you want to do?", menu) if choice == 1 break elseif choice == 2 + clipboard(parse_code(response)["julia"]) + println("Response copied to the system's clipboard!") + elseif choice == 3 response = edit_in_editor(response) println(response) - elseif choice == 3 + elseif choice == 4 description = edit_in_editor(description) prompt = format_template(template; description, examples) response = stream_completion(model, prompt) - elseif choice == 4 - response = stream_completion(model, prompt) elseif choice == 5 + response = stream_completion(model, prompt) + elseif choice == 6 response = fix_syntax_errors(model, code, error_message) elseif choice == -1 InterruptException() @@ -126,6 +136,7 @@ end """ fix_syntax_errors(model::AbstractLLM, code::AbstractString, error::AbstractString) + Fixes syntax errors in the `code` by querying the Large Language Model `model`, based on an `error` produced by the Julia parser. Returns Markdown-formatted text containing the corrected code in a Julia code block. @@ -141,9 +152,10 @@ end """ translate(model::AbstractLLM, description::AbstractString; interactive::Bool = false) -Translate the natural-language `description` of an optimization problem into + +Translate the natural-language `description` of an optimization problem into a Constraint Programming model by querying the Large Language Model `model`. -If `interactive`, the user will be prompted via the command line to inspect the +If `interactive`, the user will be prompted via the command line to inspect the intermediate outputs of the LLM, and possibly modify them. """ function translate( diff --git a/src/utils.jl b/src/utils.jl index bc9a4e2..f60e3df 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1,5 +1,6 @@ """ get_package_path() + Returns the absolute path of the root directory of `ConstraintsTranslator.jl`. """ function get_package_path() @@ -8,4 +9,4 @@ function get_package_path() error("The path of the package could not be found. This should never happen!") end return package_path -end \ No newline at end of file +end diff --git a/templates/ExtractStructure.json b/templates/ExtractStructure.json index 8a2feeb..b351568 100644 --- a/templates/ExtractStructure.json +++ b/templates/ExtractStructure.json @@ -7,7 +7,7 @@ "_type": "metadatamessage" }, { - "content": "You are an AI assistant specialized in modeling Constraint Programming (CP) problems. You have extensive knowledge of the XCSP3 Constraints and of the most used modeling patterns in Constraint Programming.\nYour task is to examine a given problem description and extract key structural information. Provide your analysis in the following format:\n\n1. Problem Description:\n- Summarize the problem statement and all of its specifications.\n\n2. Input data. Describe the format of the input data of the optimization problem. If no format is specified by the user, make sensible assumptions about one or multiple .csv files representing the problem inputs, and very concisely describe their headers.\n3. Parameter Sets:\n- Identify sets of known quantities given in the problem description. These are fixed inputs to the problem, not determined by the optimization process.\n- For each set of parameters:\n* Provide a descriptive name for the set.\n\n*Define a symbolic notation for the set using subscripts (e.g., a_ijk), specifying the meaning and the range of each index.\n\n3. Decision Variables:\n- Identify the key sets of decisions that need to be made. For each set of decision variables:\n* Provide a descriptive name for the set.\n* Specify the domain (possible values) for elements in this set, which can be either binary, integer or continuous.\n*Define a notation for the set using subscripts (e.g., x_ijk), specifying the meaning and the range of each index.\n\n4. Problem Type: determine whether the problem is a satisfaction or an optimization problem. If it is an optimization problem, provide: - a description of the objective function; - a symbolic Expression, consistently with the notation already defined. Otherwise, if the problem is a satisfaction problem, concisely state this fact.\n\n5. Constraints. Express the problem's constraint using user-provided Core Constraints. You must prefer using CP-oriented global constraints when possible. For each constraint:\n* Write a short description\n*Write the name (only the name) of Core Constraint(s) enforcing the constraint.\n*Write the scope of the constraint, that is, the indexes of the variables appearing in the constraint.\n\nList of core constraints:\n{{constraints}}\n\nIMPORTANT: - Prioritize Constraint Programming formulations over MIP formulations.\n-You must use as few variables and constraints as possible: you must avoid useless or redundant constraints.\n-You must not refer to constraints outside the Core Constraints list.\n-You must make sure that the Core Constraints are used with the appropriate arguments.\n-You must output the requested information only.", + "content": "You are an AI assistant specialized in modeling Constraint Programming (CP) problems. You have extensive knowledge of the XCSP3 Constraints and of the most used modeling patterns in Constraint Programming.\nYour task is to examine a given problem description and extract key structural information. Provide your analysis in a MarkDown document containing the following sections:\n\n# 1. Problem Description\n- Summarize the problem statement and all of its specifications.\n\n# 2. Input data\n Describe the format of the input data of the optimization problem. Use MarkDown tables where appropriate. If no format is specified by the user, make sensible assumptions about one or multiple .csv files representing the problem inputs, and very concisely describe their headers.\n\n# 3. Parameter Sets\n- Identify sets of known quantities given in the problem description. These are fixed inputs to the problem, not determined by the optimization process.\n- For each set of parameters:\n* Provide a descriptive name for the set.\n\n*Define a mathematical notation for the set in LaTeX (e.g., $a_{ijk}$), specifying the meaning and the range of each index\n\n# 4. Decision Variables\n- Identify the key sets of decisions that need to be made. For each set of decision variables:\n* Provide a descriptive name for the set.\n* Specify the domain (possible values) for elements in this set, which can be either binary, integer or continuous.\n*Define a mathematical notation for the set using LaTeX (e.g., $x_{ijk}$)\n\n# 5. Problem Type\nDetermine whether the problem is a satisfaction or an optimization problem. If it is an optimization problem, provide:\n- A description of the objective function\n- A mathematical expression using LaTeX, consistently with the LaTeX notation already defined. Otherwise, if the problem is a satisfaction problem, concisely state this fact.\n\n6. Constraints\nExpress the problem's constraint using user-provided Core Constraints. You must prefer using CP-oriented global constraints when possible. For each constraint:\n* Write a short description\n*Write the name (only the name) of Core Constraint(s) enforcing the constraint.\n*Write the scope of the constraint, that is, the indexes of the variables appearing in the constraint.\n\nList of core constraints:\n{{constraints}}\n\nIMPORTANT:\n- Prioritize Constraint Programming formulations over MIP formulations.\n-You must use as few variables and constraints as possible: you must avoid useless or redundant constraints.\n-You must not refer to constraints outside the Core Constraints list.\n-You must make sure that the Core Constraints are used with the appropriate arguments.\n-You must output the requested information only.", "variables": [ "constraints" ],