Skip to content

Commit 8da0387

Browse files
authored
Introduce chat history class (openvinotoolkit#2816)
## Description This PR introduces a new `ChatHistory` class to replace the previous vector-based chat history implementation, providing a more structured and type-safe way to handle conversation messages. The implementation uses `JsonContainer` internally for message storage and adds Python bindings for the new class. Ticket: CVS-170884 ## Checklist: - [x] Tests have been updated or added to cover the new code - [x] This patch fully addresses the ticket. <!--- If follow-up pull requests are needed, specify in description. --> - [x] I have made corresponding changes to the documentation - **N/A**
1 parent 453630b commit 8da0387

26 files changed

+821
-271
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright (C) 2025 Intel Corporation
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
#pragma once
5+
6+
#include "openvino/genai/visibility.hpp"
7+
#include "openvino/genai/json_container.hpp"
8+
9+
namespace ov {
10+
namespace genai {
11+
12+
/**
13+
* @brief ChatHistory stores conversation messages and optional metadata for chat templates.
14+
*
15+
* Manages:
16+
* - Message history (array of message objects)
17+
* - Optional tools definitions array (for function calling)
18+
* - Optional extra context object (for custom template variables)
19+
*/
20+
class OPENVINO_GENAI_EXPORTS ChatHistory {
21+
public:
22+
ChatHistory();
23+
24+
explicit ChatHistory(const JsonContainer& messages);
25+
26+
explicit ChatHistory(const std::vector<ov::AnyMap>& messages);
27+
28+
/**
29+
* @brief Construct from initializer list for convenient inline creation.
30+
*
31+
* Example:
32+
* ChatHistory history({
33+
* {{"role", "system"}, {"content", "You are helpful assistant."}},
34+
* {{"role", "user"}, {"content", "Hello"}}
35+
* });
36+
*/
37+
ChatHistory(std::initializer_list<std::initializer_list<std::pair<std::string, ov::Any>>> messages);
38+
39+
~ChatHistory();
40+
41+
ChatHistory& push_back(const JsonContainer& message);
42+
ChatHistory& push_back(const ov::AnyMap& message);
43+
ChatHistory& push_back(std::initializer_list<std::pair<std::string, ov::Any>> message);
44+
45+
void pop_back();
46+
47+
const JsonContainer& get_messages() const;
48+
JsonContainer& get_messages();
49+
50+
JsonContainer operator[](size_t index) const;
51+
JsonContainer operator[](int index) const;
52+
53+
JsonContainer first() const;
54+
JsonContainer last() const;
55+
56+
void clear();
57+
58+
size_t size() const;
59+
bool empty() const;
60+
61+
ChatHistory& set_tools(const JsonContainer& tools);
62+
const JsonContainer& get_tools() const;
63+
64+
ChatHistory& set_extra_context(const JsonContainer& extra_context);
65+
const JsonContainer& get_extra_context() const;
66+
67+
private:
68+
JsonContainer m_messages = JsonContainer::array();
69+
JsonContainer m_tools = JsonContainer::array();
70+
JsonContainer m_extra_context = JsonContainer::object();
71+
};
72+
73+
} // namespace genai
74+
} // namespace ov

src/cpp/include/openvino/genai/json_container.hpp

Lines changed: 36 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -8,30 +8,13 @@
88
#include <optional>
99
#include <initializer_list>
1010

11-
#include <nlohmann/json.hpp>
12-
1311
#include "openvino/core/any.hpp"
1412
#include "openvino/genai/visibility.hpp"
1513

1614
namespace ov {
1715
namespace genai {
1816

1917
class OPENVINO_GENAI_EXPORTS JsonContainer {
20-
private:
21-
template<typename T>
22-
struct is_json_primitive {
23-
using type = typename std::decay<T>::type;
24-
static constexpr bool value =
25-
std::is_same<type, bool>::value ||
26-
std::is_same<type, int64_t>::value ||
27-
std::is_same<type, int>::value ||
28-
std::is_same<type, double>::value ||
29-
std::is_same<type, float>::value ||
30-
std::is_same<type, std::string>::value ||
31-
std::is_same<type, const char*>::value ||
32-
std::is_same<type, std::nullptr_t>::value;
33-
};
34-
3518
public:
3619
/**
3720
* @brief Default constructor creates an empty JSON object.
@@ -41,9 +24,14 @@ class OPENVINO_GENAI_EXPORTS JsonContainer {
4124
/**
4225
* @brief Construct from JSON primitive types (bool, int64_t, double, string, etc.).
4326
*/
44-
template<typename T>
45-
JsonContainer(T&& value, typename std::enable_if<is_json_primitive<T>::value, int>::type = 0) :
46-
JsonContainer(nlohmann::ordered_json(std::forward<T>(value))) {}
27+
JsonContainer(bool value);
28+
JsonContainer(int value);
29+
JsonContainer(int64_t value);
30+
JsonContainer(double value);
31+
JsonContainer(float value);
32+
JsonContainer(const std::string& value);
33+
JsonContainer(const char* value);
34+
JsonContainer(std::nullptr_t);
4735

4836
/**
4937
* @brief Construct from initializer list of key-value pairs.
@@ -105,13 +93,14 @@ class OPENVINO_GENAI_EXPORTS JsonContainer {
10593
/**
10694
* @brief Assignment operator for JSON primitive types (bool, int64_t, double, string, etc.).
10795
*/
108-
template<typename T>
109-
typename std::enable_if<is_json_primitive<T>::value, JsonContainer&>::type
110-
operator=(T&& value) {
111-
auto json_value_ptr = get_json_value_ptr(AccessMode::Write);
112-
*json_value_ptr = nlohmann::ordered_json(std::forward<T>(value));
113-
return *this;
114-
}
96+
JsonContainer& operator=(bool value);
97+
JsonContainer& operator=(int value);
98+
JsonContainer& operator=(int64_t value);
99+
JsonContainer& operator=(double value);
100+
JsonContainer& operator=(float value);
101+
JsonContainer& operator=(const std::string& value);
102+
JsonContainer& operator=(const char* value);
103+
JsonContainer& operator=(std::nullptr_t);
115104

116105
/**
117106
* @brief Copy assignment operator.
@@ -156,24 +145,22 @@ class OPENVINO_GENAI_EXPORTS JsonContainer {
156145
* @param value JsonContainer to append
157146
* @return Reference to this container for chaining
158147
*/
159-
JsonContainer& push_back(const JsonContainer& value);
148+
JsonContainer& push_back(const JsonContainer& item);
160149

161150
/**
162151
* @brief Add JSON primitive to end of array.
163152
* If this container is not an array, it will be converted to an array.
164153
* @param value JSON primitive to append (bool, int64_t, double, string, etc.)
165154
* @return Reference to this container for chaining
166155
*/
167-
template<typename T>
168-
typename std::enable_if<is_json_primitive<T>::value, JsonContainer&>::type
169-
push_back(T&& value) {
170-
auto json_value_ptr = get_json_value_ptr(AccessMode::Write);
171-
if (!json_value_ptr->is_array()) {
172-
*json_value_ptr = nlohmann::ordered_json::array();
173-
}
174-
json_value_ptr->push_back(nlohmann::ordered_json(std::forward<T>(value)));
175-
return *this;
176-
}
156+
JsonContainer& push_back(bool value);
157+
JsonContainer& push_back(int value);
158+
JsonContainer& push_back(int64_t value);
159+
JsonContainer& push_back(double value);
160+
JsonContainer& push_back(float value);
161+
JsonContainer& push_back(const std::string& value);
162+
JsonContainer& push_back(const char* value);
163+
JsonContainer& push_back(std::nullptr_t);
177164

178165
/**
179166
* @brief Convert this container to an empty object.
@@ -230,29 +217,27 @@ class OPENVINO_GENAI_EXPORTS JsonContainer {
230217
*/
231218
std::string to_json_string(int indent = -1) const;
232219

233-
/**
234-
* @brief Convert to nlohmann::ordered_json for internal use.
235-
* @return nlohmann::ordered_json representation
236-
*/
237-
nlohmann::ordered_json to_json() const;
238-
239220
/**
240221
* @brief Get string representation of the JSON type.
241222
* @return Type name: "null", "boolean", "number", "string", "array", "object" or "unknown"
242223
*/
243224
std::string type_name() const;
244225

226+
/**
227+
* @internal
228+
* @brief Internal use only - get pointer to underlying JSON for serialization.
229+
* @return Opaque pointer to internal JSON representation
230+
*/
231+
void* _get_json_value_ptr() const;
232+
245233
private:
246-
JsonContainer(std::shared_ptr<nlohmann::ordered_json> json_ptr, const std::string& path);
247-
JsonContainer(nlohmann::ordered_json json);
234+
class JsonContainerImpl;
248235

249-
std::shared_ptr<nlohmann::ordered_json> m_json;
236+
JsonContainer(std::shared_ptr<JsonContainerImpl> impl, const std::string& path = "");
250237

251-
std::string m_path = "";
238+
std::shared_ptr<JsonContainerImpl> m_impl;
252239

253-
enum class AccessMode { Read, Write };
254-
255-
nlohmann::ordered_json* get_json_value_ptr(AccessMode mode) const;
240+
std::string m_path = "";
256241
};
257242

258243
} // namespace genai

src/cpp/include/openvino/genai/tokenizer.hpp

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@
1313
#include "openvino/genai/visibility.hpp"
1414
#include <openvino/runtime/properties.hpp>
1515

16+
#include "openvino/genai/chat_history.hpp"
17+
1618
namespace ov {
1719
namespace genai {
1820

19-
using ChatHistory = std::vector<ov::AnyMap>;
20-
using ToolDefinitions = std::vector<ov::AnyMap>;
21+
using ov::genai::JsonContainer;
22+
using ov::genai::ChatHistory;
2123

2224
using Vocab = std::unordered_map<std::string, int64_t>; // similar to huggingface .get_vocab() output format
2325

@@ -248,20 +250,20 @@ class OPENVINO_GENAI_EXPORTS Tokenizer {
248250
* For example, for Qwen family models, the prompt "1+1=" would be transformed into
249251
* <|im_start|>user\n1+1=<|im_end|>\n<|im_start|>assistant\n.
250252
*
251-
* @param history A vector of chat messages, where each message is represented as a map, e.g. [{"role": "user", "content": "prompt"}, ...].
253+
* @param history Chat history containing the conversation messages and optional tools/extra_context. Each message is a JSON-like object, e.g. [{"role": "user", "content": "prompt"}, ...].
252254
* @param add_generation_prompt Whether to add an ending that indicates the start of generation.
253255
* @param chat_template An optional custom chat template string, if not specified will be taken from the tokenizer.
254-
* @param tools An optional vector of tool definitions to be used in the chat template.
255-
* @param extra_context An optional map of additional variables to be used in the chat template.
256+
* @param tools An optional JSON-like array of tool definitions to be used in the chat template. If provided, overrides tools from chat history.
257+
* @param extra_context An optional JSON-like object with additional variables to be used in the chat template. If provided, overrides extra_context from chat history.
256258
* @return A string with the formatted and concatenated prompts from the chat history.
257259
* @throws Exception if the chat template was unable to parse the input history.
258260
*/
259261
std::string apply_chat_template(
260-
ChatHistory history,
262+
const ChatHistory& history,
261263
bool add_generation_prompt,
262264
const std::string& chat_template = {},
263-
const ToolDefinitions& tools = {},
264-
const ov::AnyMap& extra_context = {}
265+
const std::optional<JsonContainer>& tools = std::nullopt,
266+
const std::optional<JsonContainer>& extra_context = std::nullopt
265267
) const;
266268

267269
/// @brief Override a chat_template read from tokenizer_config.json.

src/cpp/src/chat_history.cpp

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// Copyright (C) 2025 Intel Corporation
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
#include "openvino/genai/chat_history.hpp"
5+
6+
namespace ov {
7+
namespace genai {
8+
9+
ChatHistory::ChatHistory() = default;
10+
11+
ChatHistory::ChatHistory(const JsonContainer& messages) : m_messages(messages) {
12+
if (!m_messages.is_array()) {
13+
OPENVINO_THROW("Chat history must be initialized with a JSON array.");
14+
}
15+
}
16+
ChatHistory::ChatHistory(const std::vector<ov::AnyMap>& messages) :
17+
m_messages(JsonContainer::array()) {
18+
for (const auto& message : messages) {
19+
m_messages.push_back(JsonContainer(message));
20+
}
21+
}
22+
23+
ChatHistory::ChatHistory(std::initializer_list<std::initializer_list<std::pair<std::string, ov::Any>>> messages) :
24+
m_messages(JsonContainer::array()) {
25+
for (const auto& message : messages) {
26+
m_messages.push_back(JsonContainer(message));
27+
}
28+
}
29+
30+
ChatHistory::~ChatHistory() = default;
31+
32+
ChatHistory& ChatHistory::push_back(const JsonContainer& message) {
33+
m_messages.push_back(message);
34+
return *this;
35+
}
36+
37+
ChatHistory& ChatHistory::push_back(const ov::AnyMap& message) {
38+
m_messages.push_back(JsonContainer(message));
39+
return *this;
40+
}
41+
42+
ChatHistory& ChatHistory::push_back(std::initializer_list<std::pair<std::string, ov::Any>> message) {
43+
m_messages.push_back(JsonContainer(message));
44+
return *this;
45+
}
46+
47+
void ChatHistory::pop_back() {
48+
if (m_messages.empty()) {
49+
OPENVINO_THROW("Cannot pop_back from an empty chat history.");
50+
}
51+
m_messages.erase(m_messages.size() - 1);
52+
}
53+
54+
const JsonContainer& ChatHistory::get_messages() const {
55+
return m_messages;
56+
}
57+
58+
JsonContainer& ChatHistory::get_messages() {
59+
return m_messages;
60+
}
61+
62+
JsonContainer ChatHistory::operator[](size_t index) const {
63+
if (index >= m_messages.size()) {
64+
OPENVINO_THROW("Index ", index, " is out of bounds for chat history of size ", m_messages.size());
65+
}
66+
return m_messages[index];
67+
}
68+
69+
JsonContainer ChatHistory::operator[](int index) const {
70+
return operator[](size_t(index));
71+
}
72+
73+
JsonContainer ChatHistory::first() const {
74+
if (m_messages.empty()) {
75+
OPENVINO_THROW("Cannot access first message of an empty chat history.");
76+
}
77+
return m_messages[0];
78+
}
79+
80+
JsonContainer ChatHistory::last() const {
81+
if (m_messages.empty()) {
82+
OPENVINO_THROW("Cannot access last message of an empty chat history.");
83+
}
84+
return m_messages[m_messages.size() - 1];
85+
}
86+
87+
void ChatHistory::clear() {
88+
m_messages.clear();
89+
}
90+
91+
size_t ChatHistory::size() const {
92+
return m_messages.size();
93+
}
94+
95+
bool ChatHistory::empty() const {
96+
return m_messages.empty();
97+
}
98+
99+
ChatHistory& ChatHistory::set_tools(const JsonContainer& tools) {
100+
if (!tools.is_array()) {
101+
OPENVINO_THROW("Tools must be an array-like JsonContainer.");
102+
}
103+
m_tools = tools;
104+
return *this;
105+
}
106+
107+
const JsonContainer& ChatHistory::get_tools() const {
108+
return m_tools;
109+
}
110+
111+
ChatHistory& ChatHistory::set_extra_context(const JsonContainer& extra_context) {
112+
if (!extra_context.is_object()) {
113+
OPENVINO_THROW("Extra context must be an object-like JsonContainer.");
114+
}
115+
m_extra_context = extra_context;
116+
return *this;
117+
}
118+
119+
const JsonContainer& ChatHistory::get_extra_context() const {
120+
return m_extra_context;
121+
}
122+
123+
} // namespace genai
124+
} // namespace ov

0 commit comments

Comments
 (0)