-
Notifications
You must be signed in to change notification settings - Fork 287
Add parsing #2772
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Add parsing #2772
Conversation
namespace genai { | ||
|
||
|
||
using ParsedMessage = std::map<std::string, std::string>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
using ParsedMessage = std::map<std::string, std::string>; | |
// TODO: will be converted to JSONLike object | |
using ParsedMessage = std::map<std::string, std::string>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR adds parsing functionality to OpenVINO GenAI, implementing both base parsers for batch processing and incremental parsers for streaming scenarios. The changes introduce parser classes for structured output processing, specifically reasoning content and tool calling, with support for both C++ and Python APIs.
- Introduces new parser base classes (
ParserBase
,IncrementalParserBase
) with concrete implementations (DeepSeekR1ReasoningParser
,Llama32PythonicParser
,BaseReasoningParser
) - Adds
TextParserStreamer
class that extendsTextStreamer
with parsing capabilities for incremental text processing - Integrates parsers into the generation pipeline through
GenerationConfig
and provides comprehensive test coverage
Reviewed Changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 10 comments.
Show a summary per file
File | Description |
---|---|
tests/python_tests/test_parsers.py |
Python test cases for parser functionality |
tests/cpp/parser.cpp |
C++ unit tests for various parser implementations |
tests/cpp/CMakeLists.txt |
Build configuration update to include nlohmann_json dependency |
src/python/py_streamers.cpp |
Python bindings for TextParserStreamer class |
src/python/py_parsers.cpp |
Python bindings for parser base classes |
src/python/py_openvino_genai.cpp |
Integration of parser module initialization |
src/python/py_generation_config.cpp |
Adds parsers field to GenerationConfig Python bindings |
src/python/openvino_genai/py_openvino_genai.pyi |
Type definitions for parser classes and updated exports |
src/python/openvino_genai/__init__.py |
Python module exports for parser classes |
src/cpp/src/text_streamer.cpp |
Implementation of TextParserStreamer class |
src/cpp/src/parsers.hpp |
Internal header with parser implementation details |
src/cpp/src/parsers.cpp |
Core parser implementations and registration logic |
src/cpp/src/llm/pipeline.cpp |
Integration of parsers into LLM generation pipeline |
src/cpp/src/generation_config.cpp |
Adds parsers field support to GenerationConfig |
src/cpp/include/openvino/genai/text_streamer.hpp |
Public header for TextParserStreamer |
src/cpp/include/openvino/genai/parsers.hpp |
Public header defining parser interfaces |
src/cpp/include/openvino/genai/llm_pipeline.hpp |
Adds parsed results field to DecodedResults |
src/cpp/include/openvino/genai/generation_config.hpp |
Adds parsers field to GenerationConfig |
samples/cpp/text_generation/parsed_output_sample.cpp |
Sample demonstrating parser usage |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
extended = stream_string[:] | ||
extended.append("") | ||
|
||
for parser in parsers: | ||
for (prev_subword, subword) in zip(extended, stream_string): |
Copilot
AI
Sep 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The zip operation creates pairs where prev_subword
comes from extended
(which has an extra empty string at the end) and subword
comes from the original stream_string
. This creates incorrect pairing - the last element of extended
will be paired with the last element of stream_string
, not providing the intended previous/current relationship.
extended = stream_string[:] | |
extended.append("") | |
for parser in parsers: | |
for (prev_subword, subword) in zip(extended, stream_string): | |
prev_subwords = [""] + stream_string[:-1] | |
for parser in parsers: | |
for (prev_subword, subword) in zip(prev_subwords, stream_string): |
Copilot uses AI. Check for mistakes.
extended = stream_string[:] | ||
extended.append("") | ||
|
||
for parser in parsers: | ||
for (prev_subword, subword) in zip(extended, stream_string): |
Copilot
AI
Sep 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The zip operation creates pairs where prev_subword
comes from extended
(which has an extra empty string at the end) and subword
comes from the original stream_string
. This creates incorrect pairing - the last element of extended
will be paired with the last element of stream_string
, not providing the intended previous/current relationship.
extended = stream_string[:] | |
extended.append("") | |
for parser in parsers: | |
for (prev_subword, subword) in zip(extended, stream_string): | |
# Pair each subword with its previous subword (first element has no previous) | |
prev_subwords = [""] + stream_string[:-1] | |
for parser in parsers: | |
for (prev_subword, subword) in zip(prev_subwords, stream_string): |
Copilot uses AI. Check for mistakes.
src/python/py_streamers.cpp
Outdated
// .def("write", | ||
// py::overload_cast<ParsedMessage&>(&TextParserStreamer::write), | ||
// py::arg("message"), | ||
// "Write is called with a ParsedMessage. Returns StreamingStatus.") |
Copilot
AI
Sep 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Commented-out code should be removed rather than left in the source. If this functionality is needed later, version control can provide the history.
// .def("write", | |
// py::overload_cast<ParsedMessage&>(&TextParserStreamer::write), | |
// py::arg("message"), | |
// "Write is called with a ParsedMessage. Returns StreamingStatus.") |
Copilot uses AI. Check for mistakes.
#include <vector> | ||
#include <cctype> | ||
#include <stdexcept> | ||
#include <bits/stdc++.h> |
Copilot
AI
Sep 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The header <bits/stdc++.h>
is non-standard and GCC-specific. Use individual standard headers instead (e.g., <algorithm>
, <iostream>
, etc.) for better portability.
#include <bits/stdc++.h> |
Copilot uses AI. Check for mistakes.
src/cpp/src/parsers.cpp
Outdated
// static initializer to register available buildin parsers | ||
static bool register_backends() { | ||
IncrementalParserBase::registered_parsers[DeepSeekR1ReasoningParser::name()] = std::make_shared<DeepSeekR1ReasoningParser>(); | ||
IncrementalParserBase::registered_parsers[DeepSeekR1ReasoningParser::name()] = std::make_shared<DeepSeekR1ReasoningParser>(); |
Copilot
AI
Sep 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Line 125 is a duplicate of line 124, registering the same parser twice under the same name. The second registration overwrites the first and should likely be removed or changed to register a different parser variant.
IncrementalParserBase::registered_parsers[DeepSeekR1ReasoningParser::name()] = std::make_shared<DeepSeekR1ReasoningParser>(); |
Copilot uses AI. Check for mistakes.
src/cpp/src/text_streamer.cpp
Outdated
// if (parser->is_active()) { | ||
m_parsed_message = parser->parse(m_parsed_message, m_text_buffer, message); | ||
// } | ||
// m_parsed_message["content"] += message; |
Copilot
AI
Sep 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Commented-out code should be removed. If the is_active()
check or content concatenation is needed, it should be implemented properly rather than left as commented code.
// m_parsed_message["content"] += message; | |
// Concatenate message to "content" field in m_parsed_message | |
if (m_parsed_message.find("content") != m_parsed_message.end() && m_parsed_message["content"].index() == 1) { // assuming std::string is at index 1 | |
m_parsed_message["content"] = std::get<std::string>(m_parsed_message["content"]) + message; | |
} else { | |
m_parsed_message["content"] = message; | |
} |
Copilot uses AI. Check for mistakes.
src/cpp/src/llm/pipeline.cpp
Outdated
if (!parsers.empty()) { | ||
for (size_t i = 0; i < res.texts.size(); ++i) { | ||
auto& message = res.texts[i]; | ||
ParsedMessage msg; |
Copilot
AI
Sep 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The parser's parse
method expects a ParsedMessage&
as input but is being called with msg
which should contain the message content. The input should be initialized with the text content: ParsedMessage input; input[\"content\"] = message; msg = parser->parse(input);
ParsedMessage msg; | |
ParsedMessage input; | |
input["content"] = message; | |
ParsedMessage msg = input; |
Copilot uses AI. Check for mistakes.
def write(self, token: typing.SupportsInt | collections.abc.Sequence[typing.SupportsInt]) -> StreamingStatus: | ||
... |
Copilot
AI
Sep 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The end()
method was removed from the TextStreamer
class definition but this appears to be an intentional API change. However, verify that all dependent code has been updated to handle the absence of this method.
Copilot uses AI. Check for mistakes.
|
||
#include "openvino/genai/llm_pipeline.hpp" | ||
#include "openvino/genai/parsers.hpp" | ||
#include "openvino/genai/text_streamer.hpp" |
Copilot
AI
Sep 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ParsingState
type is used in a using declaration but is not defined in any of the included headers. This will result in a compilation error.
#include "openvino/genai/text_streamer.hpp" | |
#include "openvino/genai/text_streamer.hpp" | |
#include "openvino/genai/parsing_state.hpp" |
Copilot uses AI. Check for mistakes.
public: | ||
CurrentStreamer(const ov::genai::Tokenizer& tokenizer) | ||
: ov::genai::TextParserStreamer(tokenizer) {} | ||
ov::genai::StreamingStatus write(const ov::genai::ParsedMessage& message) { |
Copilot
AI
Sep 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The method signature uses const ov::genai::ParsedMessage& message
but the base class method TextParserStreamer::write
expects ParsedMessage& message
(non-const reference). This signature mismatch will prevent proper virtual function override.
ov::genai::StreamingStatus write(const ov::genai::ParsedMessage& message) { | |
ov::genai::StreamingStatus write(ov::genai::ParsedMessage& message) { |
Copilot uses AI. Check for mistakes.
class Llama32PythonicParser : public ParserBase { | ||
// Does not modify original content, only extracts and adds tool calls | ||
public: | ||
Llama32PythonicParser(bool keep_original_content = true) : m_keep_original_content(keep_original_content) {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Check what default is in VLLM
src/cpp/src/llm/pipeline.cpp
Outdated
ParsedMessage msg; | ||
for (auto& parser: incremental_parsers) { | ||
// Previous is and empty message because we populate message with the full generated text. | ||
msg = parser->parse(msg, "", message); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Check the state of incremental parser and reset if necessary
|
||
class DeepSeekR1ReasoningParser : public IncrementalParserBase { | ||
private: | ||
bool m_starts_with_thinking = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Private members in public API are clumsy. Everyone can see them but can't do anything with them. Make them protected. Alternatively you can only keep string way of requesting such parsers, removing the class from public headers. If someone needs it, they can still copy the implementation and adjust introducing a new parser
450c7a8
to
b15fc5f
Compare
b15fc5f
to
290c821
Compare
Description
Ticket:
Fixes #(issue)
Checklist: