Skip to content

Commit

Permalink
Enable conversations with @ai_str macros
Browse files Browse the repository at this point in the history
Enable conversations with @ai_str macros
  • Loading branch information
svilupp authored Dec 24, 2023
2 parents 449c918 + cc4b268 commit 3b1dc0e
Show file tree
Hide file tree
Showing 11 changed files with 340 additions and 10 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- `@ai_str` macros now support multi-turn conversations. The `ai"something"` call will automatically remember the last conversation, so you can simply reply with `ai!"my-reply"`. If you send another message with `ai""`, you'll start a new conversation. Same for the asynchronous versions `aai""` and `aai!""`.

### Fixed
- Removed template `RAG/CreateQAFromContext` because it's a duplicate of `RAG/RAGCreateQAFromContext`
Expand Down
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ ai"What is the capital of France?"

The returned object is a light wrapper with a generated message in the field `:content` (eg, `ans.content`) for additional downstream processing.

> [!TIP]
> If you want to reply to the previous message, or simply continue the conversation, use `@ai!_str` (notice the bang `!`):
> ```julia
> ai!"And what is the population of it?"
> ```
You can easily inject any variables with string interpolation:
```julia
country = "Spain"
Expand All @@ -46,6 +52,7 @@ ai"What is the capital of \$(country)?"
> [!TIP]
> Use after-string-flags to select the model to be called, eg, `ai"What is the capital of France?"gpt4` (use `gpt4t` for the new GPT-4 Turbo model). Great for those extra hard questions!

For more complex prompt templates, you can use handlebars-style templating and provide variables as keyword arguments:

```julia
Expand All @@ -58,7 +65,7 @@ msg = aigenerate("What is the capital of {{country}}? Is the population larger t
> Use `asyncmap` to run multiple AI-powered tasks concurrently.
> [!TIP]
> If you use slow models (like GPT-4), you can use async version of `@ai_str` -> `@aai_str` to avoid blocking the REPL, eg, `aai"Say hi but slowly!"gpt4`
> If you use slow models (like GPT-4), you can use async version of `@ai_str` -> `@aai_str` to avoid blocking the REPL, eg, `aai"Say hi but slowly!"gpt4` (similarly `@ai!_str` -> `@aai!_str` for multi-turn conversations).
For more practical examples, see the `examples/` folder and the [Advanced Examples](#advanced-examples) section below.

Expand Down
7 changes: 6 additions & 1 deletion docs/src/getting_started.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ AIMessage("The capital of France is Paris.")

Returned object is a light wrapper with generated message in field `:content` (eg, `ans.content`) for additional downstream processing.

If you want to reply to the previous message, or simply continue the conversation, use `@ai!_str` (notice the bang `!`):
```julia
ai!"And what is the population of it?"
```

You can easily inject any variables with string interpolation:
```julia
country = "Spain"
Expand Down Expand Up @@ -86,6 +91,6 @@ AIMessage("The capital of Spain is Madrid. And yes, the population of Madrid is

Pro tip: Use `asyncmap` to run multiple AI-powered tasks concurrently.

Pro tip: If you use slow models (like GPT-4), you can use async version of `@ai_str` -> `@aai_str` to avoid blocking the REPL, eg, `aai"Say hi but slowly!"gpt4`
Pro tip: If you use slow models (like GPT-4), you can use the asynchronous version of `@ai_str` -> `@aai_str` to avoid blocking the REPL, eg, `aai"Say hi but slowly!"gpt4` (similarly `@ai!_str` -> `@aai!_str` for multi-turn conversations).

For more practical examples, see the [Various Examples](@ref) section.
2 changes: 1 addition & 1 deletion src/PromptingTools.jl
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ include("llm_openai.jl")
include("llm_ollama_managed.jl")

## Convenience utils
export @ai_str, @aai_str
export @ai_str, @aai_str, @ai!_str, @aai!_str
include("macros.jl")

## Experimental modules
Expand Down
100 changes: 95 additions & 5 deletions src/macros.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
The `ai""` string macro generates an AI response to a given prompt by using `aigenerate` under the hood.
See also `ai!""` if you want to reply to the provided message / continue the conversation.
## Arguments
- `user_prompt` (String): The input prompt for the AI model.
- `model_alias` (optional, any): Provide model alias of the AI model (see `MODEL_ALIASES`).
Expand All @@ -25,15 +27,72 @@ result = ai"What is `\$a+\$a`?"
If you want to use a different model, eg, GPT-4, you can provide its alias as a flag:
```julia
result = ai"What is `1.23 * 100 + 1`?"gpt4
result = ai"What is `1.23 * 100 + 1`?"gpt4t
# AIMessage("The answer is 124.")
```
"""
macro ai_str(user_prompt, flags...)
global CONV_HISTORY, MAX_HISTORY_LENGTH
model = isempty(flags) ? MODEL_CHAT : only(flags)
prompt = Meta.parse("\"$(escape_string(user_prompt))\"")
quote
conv = aigenerate($(esc(prompt)); model = $(esc(model)), return_all = true)
push_conversation!($(esc(CONV_HISTORY)), conv, $(esc(MAX_HISTORY_LENGTH)))
last(conv)
end
end

"""
ai!"user_prompt"[model_alias] -> AIMessage
The `ai!""` string macro is used to continue a previous conversation with the AI model.
It appends the new user prompt to the last conversation in the tracked history (in `PromptingTools.CONV_HISTORY`) and generates a response based on the entire conversation context.
If you want to see the previous conversation, you can access it via `PromptingTools.CONV_HISTORY`, which keeps at most last `PromptingTools.MAX_HISTORY_LENGTH` conversations.
## Arguments
- `user_prompt` (String): The new input prompt to be added to the existing conversation.
- `model_alias` (optional, any): Specify the model alias of the AI model to be used (see `MODEL_ALIASES`). If not provided, the default model is used.
## Returns
`AIMessage` corresponding to the new user prompt, considering the entire conversation history.
## Example
To continue a conversation:
```julia
# start conversation as normal
ai"Say hi."
# ... wait for reply and then react to it:
# continue the conversation (notice that you can change the model, eg, to more powerful one for better answer)
ai!"What do you think about that?"gpt4t
# AIMessage("Considering our previous discussion, I think that...")
```
## Usage Notes
- This macro should be used when you want to maintain the context of an ongoing conversation (ie, the last `ai""` message).
- It automatically accesses and updates the global conversation history.
- If no conversation history is found, it raises an assertion error, suggesting to initiate a new conversation using `ai""` instead.
## Important
Ensure that the conversation history is not too long to maintain relevancy and coherence in the AI's responses. The history length is managed by `MAX_HISTORY_LENGTH`.
"""
macro ai!_str(user_prompt, flags...)
global CONV_HISTORY
model = isempty(flags) ? MODEL_CHAT : only(flags)
prompt = Meta.parse("\"$(escape_string(user_prompt))\"")
quote
aigenerate($(esc(prompt)); model = $(esc(model)))
@assert !isempty($(esc(CONV_HISTORY))) "No conversation history found. Please use `ai\"\"` instead."
# grab the last conversation
old_conv = $(esc(CONV_HISTORY))[end]
conv = aigenerate(vcat(old_conv, [UserMessage($(esc(prompt)))]);
model = $(esc(model)),
return_all = true)
# replace the last conversation with the new one
$(esc(CONV_HISTORY))[end] = conv
#
last(conv)
end
end

Expand All @@ -42,6 +101,8 @@ end
Asynchronous version of `@ai_str` macro, which will log the result once it's ready.
See also `aai!""` if you want an asynchronous reply to the provided message / continue the conversation.
# Example
Send asynchronous request to GPT-4, so we don't have to wait for the response:
Expand All @@ -54,13 +115,42 @@ m = aai"Say Hi!"gpt4;
# [ Info: AIMessage> Hello! How can I assist you today?
"""
macro aai_str(user_prompt, flags...)
global CONV_HISTORY, MAX_HISTORY_LENGTH, CONV_HISTORY_LOCK
model = isempty(flags) ? MODEL_CHAT : only(flags)
prompt = Meta.parse("\"$(escape_string(user_prompt))\"")
quote
Threads.@spawn begin
m = aigenerate($(esc(prompt)); model = $(esc(model)))
@info "AIMessage> $(m.content)" # display the result once it's ready
m
conv = aigenerate($(esc(prompt)); model = $(esc(model)), return_all = true)
lock($(esc(CONV_HISTORY_LOCK))) do
push_conversation!($(esc(CONV_HISTORY)), conv, $(esc(MAX_HISTORY_LENGTH)))
end
@info "AIMessage> $(last(conv).content)" # display the result once it's ready
last(conv)
end
end
end

macro aai!_str(user_prompt, flags...)
global CONV_HISTORY, CONV_HISTORY_LOCK
model = isempty(flags) ? MODEL_CHAT : only(flags)
prompt = Meta.parse("\"$(escape_string(user_prompt))\"")
quote
@assert !isempty($(esc(CONV_HISTORY))) "No conversation history found. Please use `aai\"\"` instead."
Threads.@spawn begin
# grab the last conversation
old_conv = $(esc(CONV_HISTORY))[end]

# send to AI
conv = aigenerate(vcat(old_conv, [UserMessage($(esc(prompt)))]);
model = $(esc(model)),
return_all = true)

# replace the last conversation with the new one
lock($(esc(CONV_HISTORY_LOCK))) do
$(esc(CONV_HISTORY))[end] = conv
end
@info "AIMessage> $(last(conv).content)" # display the result once it's ready
last(conv)
end
end
end
5 changes: 5 additions & 0 deletions src/precompilation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ msg = aiextract(schema, "I want to ask {{it}}"; it = "Is this correct?", return_
image_url = "some_mock_url"
msg = aiscan(schema, "Describe the image"; image_url)

# macro calls
ai"Hello"echo
ai!"Hello again"echo
empty!(CONV_HISTORY)

# Use of Templates
template_name = :JudgeIsItTrue
msg = aigenerate(schema, template_name; it = "Is this correct?")
Expand Down
30 changes: 29 additions & 1 deletion src/user_preferences.jl
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ Check your preferences by calling `get_preferences(key::String)`.
See `PROMPT_SCHEMA` for more information.
- `MODEL_ALIASES`: A dictionary of model aliases (`alias => full_model_name`). Aliases are used to refer to models by their aliases instead of their full names to make it more convenient to use them.
See `MODEL_ALIASES` for more information.
- `MAX_HISTORY_LENGTH`: The maximum length of the conversation history. Defaults to 5. Set to `nothing` to disable history.
See `CONV_HISTORY` for more information.
At the moment it is not possible to persist changes to `MODEL_REGISTRY` across sessions.
Define your `register_model!()` calls in your `startup.jl` file to make them available across sessions or put them at the top of your script.
Expand Down Expand Up @@ -55,6 +57,7 @@ function set_preferences!(pairs::Pair{String, <:Any}...)
"MODEL_EMBEDDING",
"MODEL_ALIASES",
"PROMPT_SCHEMA",
"MAX_HISTORY_LENGTH",
]
for (key, value) in pairs
@assert key in allowed_preferences "Unknown preference '$key'! (Allowed preferences: $(join(allowed_preferences,", "))"
Expand Down Expand Up @@ -110,6 +113,22 @@ isempty(OPENAI_API_KEY) &&
const MISTRALAI_API_KEY::String = @load_preference("MISTRALAI_API_KEY",
default=get(ENV, "MISTRALAI_API_KEY", ""));

## CONVERSATION HISTORY
"""
CONV_HISTORY
Tracks the most recent conversations through the `ai_str macros`.
Preference available: MAX_HISTORY_LENGTH, which sets how many last messages should be remembered.
See also: `push_conversation!`, `resize_conversation!`
"""
const CONV_HISTORY = Vector{Vector{<:Any}}()
const CONV_HISTORY_LOCK = ReentrantLock()
const MAX_HISTORY_LENGTH = @load_preference("MAX_HISTORY_LENGTH",
default=5)::Union{Int, Nothing}

## Model registry
# A dictionary of model names and their specs (ie, name, costs per token, etc.)
# Model specs are saved in ModelSpec struct (see below)
Expand Down Expand Up @@ -288,7 +307,16 @@ registry = Dict{String, ModelSpec}("gpt-3.5-turbo" => ModelSpec("gpt-3.5-turbo",
MistralOpenAISchema(),
1.08e-7,
0.0,
"Mistral AI's hosted model for embeddings."))
"Mistral AI's hosted model for embeddings."),
"echo" => ModelSpec("echo",
TestEchoOpenAISchema(;
response = Dict(:choices => [Dict(:message => Dict(:content => "Hello!"))],
:usage => Dict(:total_tokens => 3,
:prompt_tokens => 2,
:completion_tokens => 1)), status = 200),
0.0,
0.0,
"Echo is only for testing. It always responds with 'Hello!'"))

### Model Registry Structure
@kwdef mutable struct ModelRegistry
Expand Down
71 changes: 70 additions & 1 deletion src/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -251,4 +251,73 @@ _encode_local_image(::Nothing) = String[]

# Used for image_url in aiscan to provided consistent output type
_string_to_vector(s::AbstractString) = [s]
_string_to_vector(v::Vector{<:AbstractString}) = v
_string_to_vector(v::Vector{<:AbstractString}) = v

### Conversation Management

"""
push_conversation!(conv_history, conversation::AbstractVector, max_history::Union{Int, Nothing})
Add a new conversation to the conversation history and resize the history if necessary.
This function appends a conversation to the `conv_history`, which is a vector of conversations. Each conversation is represented as a vector of `AbstractMessage` objects. After adding the new conversation, the history is resized according to the `max_history` parameter to ensure that the size of the history does not exceed the specified limit.
## Arguments
- `conv_history`: A vector that stores the history of conversations. Typically, this is `PT.CONV_HISTORY`.
- `conversation`: The new conversation to be added. It should be a vector of `AbstractMessage` objects.
- `max_history`: The maximum number of conversations to retain in the history. If `Nothing`, the history is not resized.
## Returns
The updated conversation history.
## Example
```julia
new_conversation = aigenerate("Hello World"; return_all = true)
push_conversation!(PT.CONV_HISTORY, new_conversation, 10)
```
This is done automatically by the ai"" macros.
"""
function push_conversation!(conv_history::Vector{<:Vector{<:Any}},
conversation::AbstractVector,
max_history::Union{Int, Nothing})
push!(conv_history, conversation)
resize_conversation!(conv_history, max_history)
return conv_history
end

"""
resize_conversation!(conv_history, max_history::Union{Int, Nothing})
Resize the conversation history to a specified maximum length.
This function trims the `conv_history` to ensure that its size does not exceed `max_history`. It removes the oldest conversations first if the length of `conv_history` is greater than `max_history`.
## Arguments
- `conv_history`: A vector that stores the history of conversations. Typically, this is `PT.CONV_HISTORY`.
- `max_history`: The maximum number of conversations to retain in the history. If `Nothing`, the history is not resized.
## Returns
The resized conversation history.
## Example
```julia
resize_conversation!(PT.CONV_HISTORY, PT.MAX_HISTORY_LENGTH)
```
After the function call, `conv_history` will contain only the 10 most recent conversations.
This is done automatically by the ai"" macros.
"""
function resize_conversation!(conv_history,
max_history::Union{Int, Nothing})
if isnothing(max_history)
return
end

while length(conv_history) > max_history
popfirst!(conv_history)
end
return conv_history
end
Loading

0 comments on commit 3b1dc0e

Please sign in to comment.