Skip to content

Commit

Permalink
feat: Add AnnotationMessage implementation
Browse files Browse the repository at this point in the history
- Add AnnotationMessage struct for metadata and documentation
- Implement annotate! utility for single/vector messages
- Add comprehensive tests for construction and rendering
- Ensure proper rendering skipping across all providers
- Add serialization support
  • Loading branch information
devin-ai-integration[bot] committed Nov 26, 2024
1 parent fa2d724 commit 83df820
Show file tree
Hide file tree
Showing 3 changed files with 337 additions and 18 deletions.
99 changes: 81 additions & 18 deletions src/messages.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ abstract type AbstractMessage end
abstract type AbstractChatMessage <: AbstractMessage end # with text-based content
abstract type AbstractDataMessage <: AbstractMessage end # with data-based content, eg, embeddings
abstract type AbstractTracerMessage{T <: AbstractMessage} <: AbstractMessage end # message with annotation that exposes the underlying message
abstract type AbstractAnnotationMessage <: AbstractMessage end # message with metadata that is never sent to LLMs
# Complementary type for tracing, follows the same API as TracerMessage
abstract type AbstractTracer{T <: Any} end

Expand Down Expand Up @@ -41,9 +42,9 @@ end
"""
UserMessage
A message type for user-generated text-based responses.
A message type for user-generated text-based responses.
Consumed by `ai*` functions to generate responses.
# Fields
- `content::T`: The content of the message.
- `variables::Vector{Symbol}`: The variables in the message.
Expand All @@ -68,9 +69,9 @@ end
"""
UserMessageWithImages
A message type for user-generated text-based responses with images.
A message type for user-generated text-based responses with images.
Consumed by `ai*` functions to generate responses.
# Fields
- `content::T`: The content of the message.
- `image_url::Vector{String}`: The URLs of the images.
Expand All @@ -97,9 +98,9 @@ end
"""
AIMessage
A message type for AI-generated text-based responses.
A message type for AI-generated text-based responses.
Returned by `aigenerate`, `aiclassify`, and `aiscan` functions.
# Fields
- `content::Union{AbstractString, Nothing}`: The content of the message.
- `status::Union{Int, Nothing}`: The status of the message from the API.
Expand Down Expand Up @@ -131,9 +132,9 @@ end
"""
DataMessage
A message type for AI-generated data-based responses, ie, different `content` than text.
A message type for AI-generated data-based responses, ie, different `content` than text.
Returned by `aiextract`, and `aiextract` functions.
# Fields
- `content::Union{AbstractString, Nothing}`: The content of the message.
- `status::Union{Int, Nothing}`: The status of the message from the API.
Expand Down Expand Up @@ -163,8 +164,8 @@ end
"""
ToolMessage
A message type for tool calls.
A message type for tool calls.
It represents both the request (fields `args`, `name`) and the response (field `content`).
# Fields
Expand All @@ -188,9 +189,9 @@ end
"""
AIToolRequest
A message type for AI-generated tool requests.
A message type for AI-generated tool requests.
Returned by `aitools` functions.
# Fields
- `content::Union{AbstractString, Nothing}`: The content of the message.
- `tool_calls::Vector{ToolMessage}`: The vector of tool call requests.
Expand Down Expand Up @@ -224,11 +225,63 @@ Base.@kwdef struct AIToolRequest{T <: Union{AbstractString, Nothing}} <: Abstrac
sample_id::Union{Nothing, Int} = nothing
_type::Symbol = :aitoolrequest
end
"Get the vector of tool call requests from an AIToolRequest/message."
tool_calls(msg::AIToolRequest) = msg.tool_calls
tool_calls(msg::AbstractMessage) = ToolMessage[]
tool_calls(msg::ToolMessage) = [msg]
tool_calls(msg::AbstractTracerMessage) = tool_calls(msg.object)

"""
AnnotationMessage{T} <: AbstractAnnotationMessage
A message type for adding metadata and documentation to conversation histories without affecting LLM context.
Fields:
- `content::T`: The main content of the annotation
- `extras::Dict{Symbol,Any}`: Additional metadata as key-value pairs
- `tags::Vector{Symbol}`: Tags for categorizing the annotation
- `comment::String`: Human-readable comment (not used for automatic operations)
- `run_id::Union{Nothing,Int}`: Optional run ID for tracking message provenance
- `_type::Symbol`: Internal type identifier
The AnnotationMessage is designed to bundle key information and documentation together with conversation data
without sending any of it to large language models.
"""
Base.@kwdef mutable struct AnnotationMessage{T} <: AbstractAnnotationMessage
content::T
extras::Dict{Symbol,Any} = Dict{Symbol,Any}()
tags::Vector{Symbol} = Symbol[]
comment::String = ""
run_id::Union{Nothing,Int} = get_run_id()
_type::Symbol = :annotationmessage
end

# Constructor that accepts content as first argument
AnnotationMessage(content; kwargs...) = AnnotationMessage(; content=content, kwargs...)

"""
annotate!(messages::Vector{<:AbstractMessage}, content; kwargs...)
annotate!(message::AbstractMessage, content; kwargs...)
Add an annotation message to a vector of messages or wrap a single message in a vector with an annotation.
The annotation is inserted after any existing annotation messages to maintain logical grouping.
All kwargs are passed to the AnnotationMessage constructor.
Returns the modified vector of messages.
"""
function annotate!(messages::Vector{<:AbstractMessage}, content; kwargs...)
msg = AnnotationMessage(content; kwargs...)
# Find the last annotation message
last_annotation_idx = findlast(isabstractannotationmessage, messages)
if isnothing(last_annotation_idx)
# No existing annotations, insert at start
insert!(messages, 1, msg)
else
# Insert after last annotation
insert!(messages, last_annotation_idx + 1, msg)
end
return messages
end

function annotate!(message::AbstractMessage, content; kwargs...)
annotate!([message], content; kwargs...)
end

### Other Message methods
# content-only constructor
Expand All @@ -251,6 +304,7 @@ isaimessage(m::Any) = m isa AIMessage
istoolmessage(m::Any) = m isa ToolMessage
isaitoolrequest(m::Any) = m isa AIToolRequest
istracermessage(m::Any) = m isa AbstractTracerMessage
isabstractannotationmessage(m::Any) = m isa AbstractAnnotationMessage
isusermessage(m::AbstractTracerMessage) = isusermessage(m.object)
isusermessagewithimages(m::AbstractTracerMessage) = isusermessagewithimages(m.object)
issystemmessage(m::AbstractTracerMessage) = issystemmessage(m.object)
Expand Down Expand Up @@ -557,7 +611,8 @@ function StructTypes.subtypes(::Type{AbstractMessage})
systemmessage = SystemMessage,
metadatamessage = MetadataMessage,
datamessage = DataMessage,
tracermessage = TracerMessage)
tracermessage = TracerMessage,
annotationmessage = AnnotationMessage)
end

StructTypes.StructType(::Type{AbstractChatMessage}) = StructTypes.AbstractType()
Expand Down Expand Up @@ -592,6 +647,7 @@ StructTypes.StructType(::Type{AIMessage}) = StructTypes.Struct()
StructTypes.StructType(::Type{DataMessage}) = StructTypes.Struct()
StructTypes.StructType(::Type{TracerMessage}) = StructTypes.Struct() # Ignore mutability once we serialize
StructTypes.StructType(::Type{TracerMessageLike}) = StructTypes.Struct() # Ignore mutability once we serialize
StructTypes.StructType(::Type{AnnotationMessage}) = StructTypes.Struct()

### Utilities for Pretty Printing
"""
Expand All @@ -615,6 +671,8 @@ function pprint(io::IO, msg::AbstractMessage; text_width::Int = displaysize(io)[
"AI Tool Request"
elseif msg isa ToolMessage
"Tool Message"
elseif msg isa AnnotationMessage
"Annotation Message"
else
"Unknown Message"
end
Expand All @@ -633,6 +691,11 @@ function pprint(io::IO, msg::AbstractMessage; text_width::Int = displaysize(io)[
elseif istoolmessage(msg)
isnothing(msg.content) ? string("Name: ", msg.name, ", Args: ", msg.raw) :
string(msg.content)
elseif msg isa AnnotationMessage
content_str = wrap_string(msg.content, text_width)
tags_str = isempty(msg.tags) ? "" : "\nTags: " * join(msg.tags, ", ")
comment_str = isempty(msg.comment) ? "" : "\nComment: " * msg.comment
string(content_str, tags_str, comment_str)
else
wrap_string(msg.content, text_width)
end
Expand Down
94 changes: 94 additions & 0 deletions test/annotation_messages_render.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using Test
using PromptingTools
using PromptingTools: OpenAISchema, AnthropicSchema, OllamaSchema, GoogleSchema, TestEchoOpenAISchema, render

@testset "Annotation Message Rendering" begin
# Create a mix of messages including annotation messages
messages = [
SystemMessage("Be helpful"),
AnnotationMessage("This is metadata", extras=Dict{Symbol,Any}(:key => "value")),
UserMessage("Hello"),
AnnotationMessage("More metadata"),
AIMessage("Hi there!")
]

# Additional edge cases
messages_complex = [
AnnotationMessage("Metadata 1", extras=Dict{Symbol,Any}(:key => "value")),
AnnotationMessage("Metadata 2", extras=Dict{Symbol,Any}(:key2 => "value2")),
SystemMessage("Be helpful"),
AnnotationMessage("Metadata 3", tags=[:important]),
UserMessage("Hello"),
AnnotationMessage("Metadata 4", comment="For debugging"),
AIMessage("Hi there!"),
AnnotationMessage("Metadata 5", extras=Dict{Symbol,Any}(:key3 => "value3"))
]

@testset "Basic Message Filtering" begin
# Test OpenAI Schema with TestEcho
schema = TestEchoOpenAISchema(
response=Dict(
"choices" => [Dict("message" => Dict("content" => "Test response", "role" => "assistant"), "index" => 0, "finish_reason" => "stop")],
"usage" => Dict("prompt_tokens" => 10, "completion_tokens" => 20, "total_tokens" => 30),
"model" => "gpt-3.5-turbo",
"id" => "test-id",
"object" => "chat.completion",
"created" => 1234567890
),
status=200
)
rendered = render(schema, messages)
@test length(rendered) == 3 # Should only have system, user, and AI messages
@test all(msg["role"] in ["system", "user", "assistant"] for msg in rendered)
@test !any(msg -> contains(msg["content"], "metadata"), rendered)

# Test Anthropic Schema
rendered = render(AnthropicSchema(), messages)
@test length(rendered.conversation) == 2 # Should have user and AI messages
@test !isnothing(rendered.system) # System message should be preserved separately
@test all(msg["role"] in ["user", "assistant"] for msg in rendered.conversation)
@test !contains(rendered.system, "metadata") # Check system message
@test !any(msg -> any(content -> contains(content["text"], "metadata"), msg["content"]), rendered.conversation)

# Test Ollama Schema
rendered = render(OllamaSchema(), messages)
@test length(rendered) == 3 # Should only have system, user, and AI messages
@test all(msg["role"] in ["system", "user", "assistant"] for msg in rendered)
@test !any(msg -> contains(msg["content"], "metadata"), rendered)

# Test Google Schema
rendered = render(GoogleSchema(), messages)
@test length(rendered) == 2 # Google schema combines system message with first user message
@test all(msg[:role] in ["user", "model"] for msg in rendered) # Google uses "model" instead of "assistant"
@test !any(msg -> any(part -> contains(part["text"], "metadata"), msg[:parts]), rendered)
end

@testset "Complex Edge Cases" begin
# Test with multiple consecutive annotation messages
for schema in [TestEchoOpenAISchema(), AnthropicSchema(), OllamaSchema(), GoogleSchema()]
rendered = render(schema, messages_complex)

if schema isa AnthropicSchema
@test length(rendered.conversation) == 2 # user and AI only
@test !isnothing(rendered.system) # system preserved
else
@test length(rendered) == (schema isa GoogleSchema ? 2 : 3) # Google schema combines system with user message
end

# Test no metadata leaks through
for i in 1:5
if schema isa GoogleSchema
# Google schema uses a different structure
@test !any(msg -> any(part -> contains(part["text"], "Metadata $i"), msg[:parts]), rendered)
elseif schema isa AnthropicSchema
# Check each message's content array for metadata
@test !any(msg -> any(content -> contains(content["text"], "Metadata $i"), msg["content"]), rendered.conversation)
@test !contains(rendered.system, "Metadata $i")
else
# OpenAI and Ollama schemas
@test !any(msg -> contains(msg["content"], "Metadata $i"), rendered)
end
end
end
end
end
Loading

0 comments on commit 83df820

Please sign in to comment.