Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
87d0e32
Add json container class
yatarkan Oct 6, 2025
c85d316
Enable int and json container types in any to json function
yatarkan Oct 6, 2025
dc5f639
Add unit tests for json container
yatarkan Oct 6, 2025
8d97cb0
Remove commented code
yatarkan Oct 6, 2025
6fc758e
Fix equality tests
yatarkan Oct 6, 2025
15f55b0
Add tests for copy, share and move
yatarkan Oct 6, 2025
9501674
Merge branch 'master' into yt/json-container
yatarkan Oct 6, 2025
ac4dcf4
Rename methods
yatarkan Oct 8, 2025
1bec5e1
Add static methods for creating empty json array and object container
yatarkan Oct 8, 2025
513721e
Fix spelling
yatarkan Oct 8, 2025
aa81b51
Remove redundant include
yatarkan Oct 9, 2025
3c888f2
Add test for json container modification
yatarkan Oct 9, 2025
bfa7687
Add chat history class
yatarkan Oct 9, 2025
54f7815
Update apply chat template with chat history
yatarkan Oct 9, 2025
07faaa0
Fix chat history usages in pipelines
yatarkan Oct 9, 2025
cad08f9
Add py object to json container utility function
yatarkan Oct 9, 2025
aabc834
Update apply chat template python binding with chat history
yatarkan Oct 9, 2025
6b61440
Remove json keys sorting in apply chat template test
yatarkan Oct 9, 2025
b884418
Merge branch 'master' into yt/json-chat-history
yatarkan Oct 9, 2025
bb24aee
Add chat history class bindings
yatarkan Oct 10, 2025
b3fc29f
Update python bindings
yatarkan Oct 10, 2025
b0b97d5
Add assert hf equals genai utility function
yatarkan Oct 10, 2025
befe3b6
Add test with chat history instance in apply chat template
yatarkan Oct 10, 2025
81e0f25
Move chat history bindings to separate file
yatarkan Oct 10, 2025
ac133cc
Fix spacing
yatarkan Oct 10, 2025
3aac1be
Fix python bindings
yatarkan Oct 10, 2025
6af46db
Switch json container to PIMPL, add json adl serializer, update tests…
yatarkan Oct 14, 2025
3274723
Fix JS bindings for new chat history
yatarkan Oct 14, 2025
6074863
Merge branch 'master' into yt/json-chat-history
yatarkan Oct 14, 2025
ef16c97
Update tokenizer JS tests to match new chat history
yatarkan Oct 14, 2025
6c80af0
Fix lint
yatarkan Oct 15, 2025
32d29fb
Merge branch 'master' into yt/json-chat-history
yatarkan Oct 15, 2025
aa10568
Add tools and extra context to chat history
yatarkan Oct 15, 2025
bf55512
Make tools and extra context optional in apply chat template with pri…
yatarkan Oct 15, 2025
2ae2636
Update python bindings
yatarkan Oct 15, 2025
40f8576
Update JS bindings for new apply chat template
yatarkan Oct 15, 2025
c63d0e7
Add JS tests for tools and extra context in apply chat template
yatarkan Oct 15, 2025
f8fbc5c
Add python test for checking tools/extra_context in chat history and …
yatarkan Oct 15, 2025
cc4aa64
Merge branch 'master' into yt/json-chat-history
yatarkan Oct 15, 2025
adf10a9
Fix JS tokenizer tests
yatarkan Oct 15, 2025
e7206ca
Merge branch 'master' into yt/json-chat-history
yatarkan Oct 15, 2025
d75a22c
Add todo
yatarkan Oct 16, 2025
662aa38
Add utility method for converting json container to py object
yatarkan Oct 16, 2025
48cec9d
Reuse json_container_to_py_object method
yatarkan Oct 16, 2025
64ec9c6
Merge branch 'master' into yt/json-chat-history
yatarkan Oct 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions src/cpp/include/openvino/genai/chat_history.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright (C) 2025 Intel Corporation
// SPDX-License-Identifier: Apache-2.0

#pragma once

#include "openvino/genai/visibility.hpp"
#include "openvino/genai/json_container.hpp"

namespace ov {
namespace genai {

/**
* @brief ChatHistory stores conversation messages and optional metadata for chat templates.
*
* Manages:
* - Message history (array of message objects)
* - Optional tools definitions array (for function calling)
* - Optional extra context object (for custom template variables)
*/
class OPENVINO_GENAI_EXPORTS ChatHistory {
public:
ChatHistory();

explicit ChatHistory(const JsonContainer& messages);

explicit ChatHistory(const std::vector<ov::AnyMap>& messages);

/**
* @brief Construct from initializer list for convenient inline creation.
*
* Example:
* ChatHistory history({
* {{"role", "system"}, {"content", "You are helpful assistant."}},
* {{"role", "user"}, {"content", "Hello"}}
* });
*/
ChatHistory(std::initializer_list<std::initializer_list<std::pair<std::string, ov::Any>>> messages);

~ChatHistory();

ChatHistory& push_back(const JsonContainer& message);
ChatHistory& push_back(const ov::AnyMap& message);
ChatHistory& push_back(std::initializer_list<std::pair<std::string, ov::Any>> message);

void pop_back();

const JsonContainer& get_messages() const;
JsonContainer& get_messages();

JsonContainer operator[](size_t index) const;
JsonContainer operator[](int index) const;

JsonContainer first() const;
JsonContainer last() const;

void clear();

size_t size() const;
bool empty() const;

ChatHistory& set_tools(const JsonContainer& tools);
const JsonContainer& get_tools() const;

ChatHistory& set_extra_context(const JsonContainer& extra_context);
const JsonContainer& get_extra_context() const;

private:
JsonContainer m_messages = JsonContainer::array();
JsonContainer m_tools = JsonContainer::array();
JsonContainer m_extra_context = JsonContainer::object();
};

} // namespace genai
} // namespace ov
87 changes: 36 additions & 51 deletions src/cpp/include/openvino/genai/json_container.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,13 @@
#include <optional>
#include <initializer_list>

#include <nlohmann/json.hpp>

#include "openvino/core/any.hpp"
#include "openvino/genai/visibility.hpp"

namespace ov {
namespace genai {

class OPENVINO_GENAI_EXPORTS JsonContainer {
private:
template<typename T>
struct is_json_primitive {
using type = typename std::decay<T>::type;
static constexpr bool value =
std::is_same<type, bool>::value ||
std::is_same<type, int64_t>::value ||
std::is_same<type, int>::value ||
std::is_same<type, double>::value ||
std::is_same<type, float>::value ||
std::is_same<type, std::string>::value ||
std::is_same<type, const char*>::value ||
std::is_same<type, std::nullptr_t>::value;
};

public:
/**
* @brief Default constructor creates an empty JSON object.
Expand All @@ -41,9 +24,14 @@ class OPENVINO_GENAI_EXPORTS JsonContainer {
/**
* @brief Construct from JSON primitive types (bool, int64_t, double, string, etc.).
*/
template<typename T>
JsonContainer(T&& value, typename std::enable_if<is_json_primitive<T>::value, int>::type = 0) :
JsonContainer(nlohmann::ordered_json(std::forward<T>(value))) {}
JsonContainer(bool value);
JsonContainer(int value);
JsonContainer(int64_t value);
JsonContainer(double value);
JsonContainer(float value);
JsonContainer(const std::string& value);
JsonContainer(const char* value);
JsonContainer(std::nullptr_t);

/**
* @brief Construct from initializer list of key-value pairs.
Expand Down Expand Up @@ -105,13 +93,14 @@ class OPENVINO_GENAI_EXPORTS JsonContainer {
/**
* @brief Assignment operator for JSON primitive types (bool, int64_t, double, string, etc.).
*/
template<typename T>
typename std::enable_if<is_json_primitive<T>::value, JsonContainer&>::type
operator=(T&& value) {
auto json_value_ptr = get_json_value_ptr(AccessMode::Write);
*json_value_ptr = nlohmann::ordered_json(std::forward<T>(value));
return *this;
}
JsonContainer& operator=(bool value);
JsonContainer& operator=(int value);
JsonContainer& operator=(int64_t value);
JsonContainer& operator=(double value);
JsonContainer& operator=(float value);
JsonContainer& operator=(const std::string& value);
JsonContainer& operator=(const char* value);
JsonContainer& operator=(std::nullptr_t);

/**
* @brief Copy assignment operator.
Expand Down Expand Up @@ -156,24 +145,22 @@ class OPENVINO_GENAI_EXPORTS JsonContainer {
* @param value JsonContainer to append
* @return Reference to this container for chaining
*/
JsonContainer& push_back(const JsonContainer& value);
JsonContainer& push_back(const JsonContainer& item);

/**
* @brief Add JSON primitive to end of array.
* If this container is not an array, it will be converted to an array.
* @param value JSON primitive to append (bool, int64_t, double, string, etc.)
* @return Reference to this container for chaining
*/
template<typename T>
typename std::enable_if<is_json_primitive<T>::value, JsonContainer&>::type
push_back(T&& value) {
auto json_value_ptr = get_json_value_ptr(AccessMode::Write);
if (!json_value_ptr->is_array()) {
*json_value_ptr = nlohmann::ordered_json::array();
}
json_value_ptr->push_back(nlohmann::ordered_json(std::forward<T>(value)));
return *this;
}
JsonContainer& push_back(bool value);
JsonContainer& push_back(int value);
JsonContainer& push_back(int64_t value);
JsonContainer& push_back(double value);
JsonContainer& push_back(float value);
JsonContainer& push_back(const std::string& value);
JsonContainer& push_back(const char* value);
JsonContainer& push_back(std::nullptr_t);

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

/**
* @brief Convert to nlohmann::ordered_json for internal use.
* @return nlohmann::ordered_json representation
*/
nlohmann::ordered_json to_json() const;

/**
* @brief Get string representation of the JSON type.
* @return Type name: "null", "boolean", "number", "string", "array", "object" or "unknown"
*/
std::string type_name() const;

/**
* @internal
* @brief Internal use only - get pointer to underlying JSON for serialization.
* @return Opaque pointer to internal JSON representation
*/
void* _get_json_value_ptr() const;

private:
JsonContainer(std::shared_ptr<nlohmann::ordered_json> json_ptr, const std::string& path);
JsonContainer(nlohmann::ordered_json json);
class JsonContainerImpl;

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

std::string m_path = "";
std::shared_ptr<JsonContainerImpl> m_impl;

enum class AccessMode { Read, Write };

nlohmann::ordered_json* get_json_value_ptr(AccessMode mode) const;
std::string m_path = "";
};

} // namespace genai
Expand Down
18 changes: 10 additions & 8 deletions src/cpp/include/openvino/genai/tokenizer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
#include "openvino/genai/visibility.hpp"
#include <openvino/runtime/properties.hpp>

#include "openvino/genai/chat_history.hpp"

namespace ov {
namespace genai {

using ChatHistory = std::vector<ov::AnyMap>;
using ToolDefinitions = std::vector<ov::AnyMap>;
using ov::genai::JsonContainer;
using ov::genai::ChatHistory;

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

Expand Down Expand Up @@ -248,20 +250,20 @@ class OPENVINO_GENAI_EXPORTS Tokenizer {
* For example, for Qwen family models, the prompt "1+1=" would be transformed into
* <|im_start|>user\n1+1=<|im_end|>\n<|im_start|>assistant\n.
*
* @param history A vector of chat messages, where each message is represented as a map, e.g. [{"role": "user", "content": "prompt"}, ...].
* @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"}, ...].
* @param add_generation_prompt Whether to add an ending that indicates the start of generation.
* @param chat_template An optional custom chat template string, if not specified will be taken from the tokenizer.
* @param tools An optional vector of tool definitions to be used in the chat template.
* @param extra_context An optional map of additional variables to be used in the chat template.
* @param tools An optional JSON-like array of tool definitions to be used in the chat template. If provided, overrides tools from chat history.
* @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.
* @return A string with the formatted and concatenated prompts from the chat history.
* @throws Exception if the chat template was unable to parse the input history.
*/
std::string apply_chat_template(
ChatHistory history,
const ChatHistory& history,
bool add_generation_prompt,
const std::string& chat_template = {},
const ToolDefinitions& tools = {},
const ov::AnyMap& extra_context = {}
const std::optional<JsonContainer>& tools = std::nullopt,
const std::optional<JsonContainer>& extra_context = std::nullopt
) const;

/// @brief Override a chat_template read from tokenizer_config.json.
Expand Down
124 changes: 124 additions & 0 deletions src/cpp/src/chat_history.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Copyright (C) 2025 Intel Corporation
// SPDX-License-Identifier: Apache-2.0

#include "openvino/genai/chat_history.hpp"

namespace ov {
namespace genai {

ChatHistory::ChatHistory() = default;

ChatHistory::ChatHistory(const JsonContainer& messages) : m_messages(messages) {
if (!m_messages.is_array()) {
OPENVINO_THROW("Chat history must be initialized with a JSON array.");
}
}
ChatHistory::ChatHistory(const std::vector<ov::AnyMap>& messages) :
m_messages(JsonContainer::array()) {
for (const auto& message : messages) {
m_messages.push_back(JsonContainer(message));
}
}

ChatHistory::ChatHistory(std::initializer_list<std::initializer_list<std::pair<std::string, ov::Any>>> messages) :
m_messages(JsonContainer::array()) {
for (const auto& message : messages) {
m_messages.push_back(JsonContainer(message));
}
}

ChatHistory::~ChatHistory() = default;

ChatHistory& ChatHistory::push_back(const JsonContainer& message) {
m_messages.push_back(message);
return *this;
}

ChatHistory& ChatHistory::push_back(const ov::AnyMap& message) {
m_messages.push_back(JsonContainer(message));
return *this;
}

ChatHistory& ChatHistory::push_back(std::initializer_list<std::pair<std::string, ov::Any>> message) {
m_messages.push_back(JsonContainer(message));
return *this;
}

void ChatHistory::pop_back() {
if (m_messages.empty()) {
OPENVINO_THROW("Cannot pop_back from an empty chat history.");
}
m_messages.erase(m_messages.size() - 1);
}

const JsonContainer& ChatHistory::get_messages() const {
return m_messages;
}

JsonContainer& ChatHistory::get_messages() {
return m_messages;
}

JsonContainer ChatHistory::operator[](size_t index) const {
if (index >= m_messages.size()) {
OPENVINO_THROW("Index ", index, " is out of bounds for chat history of size ", m_messages.size());
}
return m_messages[index];
}

JsonContainer ChatHistory::operator[](int index) const {
return operator[](size_t(index));
}

JsonContainer ChatHistory::first() const {
if (m_messages.empty()) {
OPENVINO_THROW("Cannot access first message of an empty chat history.");
}
return m_messages[0];
}

JsonContainer ChatHistory::last() const {
if (m_messages.empty()) {
OPENVINO_THROW("Cannot access last message of an empty chat history.");
}
return m_messages[m_messages.size() - 1];
}

void ChatHistory::clear() {
m_messages.clear();
}

size_t ChatHistory::size() const {
return m_messages.size();
}

bool ChatHistory::empty() const {
return m_messages.empty();
}

ChatHistory& ChatHistory::set_tools(const JsonContainer& tools) {
if (!tools.is_array()) {
OPENVINO_THROW("Tools must be an array-like JsonContainer.");
}
m_tools = tools;
return *this;
}

const JsonContainer& ChatHistory::get_tools() const {
return m_tools;
}

ChatHistory& ChatHistory::set_extra_context(const JsonContainer& extra_context) {
if (!extra_context.is_object()) {
OPENVINO_THROW("Extra context must be an object-like JsonContainer.");
}
m_extra_context = extra_context;
return *this;
}

const JsonContainer& ChatHistory::get_extra_context() const {
return m_extra_context;
}

} // namespace genai
} // namespace ov
Loading
Loading