Ajout sample CPP

This commit is contained in:
2026-02-21 20:48:10 +01:00
parent d8957625f8
commit f23acc8c1c
15 changed files with 985 additions and 4 deletions

View File

@@ -0,0 +1,230 @@
#include "Conversation.hpp"
#include <boost/beast/websocket/ssl.hpp>
#include <boost/beast/websocket.hpp>
#include <boost/beast/ssl.hpp>
#include <boost/beast/core/detail/base64.hpp>
#include <boost/asio/connect.hpp>
#include <boost/algorithm/string.hpp>
#include <iostream>
#include <sstream>
#include <openssl/ssl.h>
using tcp = boost::asio::ip::tcp;
namespace ssl = boost::asio::ssl;
namespace websocket = boost::beast::websocket;
namespace beast = boost::beast;
static std::string base64Encode(const std::vector<char>& data) {
auto encodedSize = beast::detail::base64::encoded_size(data.size());
std::string out(encodedSize, '\0');
beast::detail::base64::encode(&out[0], data.data(), data.size());
return out;
}
static std::vector<char> base64Decode(const std::string& str) {
auto decodedSize = beast::detail::base64::decoded_size(str.size());
std::vector<char> out(decodedSize);
auto result = beast::detail::base64::decode(out.data(), str.data(), str.size());
out.resize(result.first);
return out;
}
static std::string toString(const nlohmann::json& j){
if(j.is_string()) return j.get<std::string>();
if(j.is_number_integer()) return std::to_string(j.get<int64_t>());
return j.dump();
}
Conversation::Conversation(const std::string& agentId, bool requiresAuth,
std::shared_ptr<AudioInterface> audioInterface,
CallbackAgentResponse callbackAgentResponse,
CallbackAgentResponseCorrection callbackAgentResponseCorrection,
CallbackUserTranscript callbackUserTranscript,
CallbackLatencyMeasurement callbackLatencyMeasurement)
: agentId_(agentId),
requiresAuth_(requiresAuth),
audioInterface_(std::move(audioInterface)),
callbackAgentResponse_(std::move(callbackAgentResponse)),
callbackAgentResponseCorrection_(std::move(callbackAgentResponseCorrection)),
callbackUserTranscript_(std::move(callbackUserTranscript)),
callbackLatencyMeasurement_(std::move(callbackLatencyMeasurement)) {
sslCtx_.set_default_verify_paths();
}
Conversation::~Conversation() {
endSession();
}
void Conversation::startSession() {
shouldStop_.store(false);
workerThread_ = std::thread(&Conversation::run, this);
}
void Conversation::endSession() {
shouldStop_.store(true);
if (ws_) {
beast::error_code ec;
ws_->close(websocket::close_code::normal, ec);
}
if (audioInterface_) {
audioInterface_->stop();
}
if (workerThread_.joinable()) {
workerThread_.join();
}
}
std::string Conversation::waitForSessionEnd() {
if (workerThread_.joinable()) {
workerThread_.join();
}
return conversationId_;
}
void Conversation::sendUserMessage(const std::string& text) {
if (!ws_) {
throw std::runtime_error("Session not started");
}
nlohmann::json j = {
{"type", "user_message"},
{"text", text}
};
ws_->write(boost::asio::buffer(j.dump()));
}
void Conversation::registerUserActivity() {
if (!ws_) throw std::runtime_error("Session not started");
nlohmann::json j = {{"type", "user_activity"}};
ws_->write(boost::asio::buffer(j.dump()));
}
void Conversation::sendContextualUpdate(const std::string& content) {
if (!ws_) throw std::runtime_error("Session not started");
nlohmann::json j = {{"type", "contextual_update"}, {"content", content}};
ws_->write(boost::asio::buffer(j.dump()));
}
std::string Conversation::getWssUrl() const {
// Hard-coded base env for demo; in production you'd call ElevenLabs env endpoint.
std::ostringstream oss;
oss << "wss://api.elevenlabs.io/v1/convai/conversation?agent_id=" << agentId_;
return oss.str();
}
void Conversation::run() {
try {
auto url = getWssUrl();
std::string protocol, host, target;
unsigned short port = 443;
// Very naive parse: wss://host[:port]/path?query
if (boost::starts_with(url, "wss://")) {
protocol = "wss";
host = url.substr(6);
} else {
throw std::runtime_error("Only wss:// URLs supported in this demo");
}
auto slashPos = host.find('/');
if (slashPos == std::string::npos) {
target = "/";
} else {
target = host.substr(slashPos);
host = host.substr(0, slashPos);
}
auto colonPos = host.find(':');
if (colonPos != std::string::npos) {
port = static_cast<unsigned short>(std::stoi(host.substr(colonPos + 1)));
host = host.substr(0, colonPos);
}
tcp::resolver resolver(ioc_);
auto const results = resolver.resolve(host, std::to_string(port));
beast::ssl_stream<tcp::socket> stream(ioc_, sslCtx_);
boost::asio::connect(beast::get_lowest_layer(stream), results);
if (!SSL_set_tlsext_host_name(stream.native_handle(), host.c_str())) {
throw std::runtime_error("Failed to set SNI hostname on SSL stream");
}
stream.handshake(ssl::stream_base::client);
ws_ = std::make_unique<websocket_t>(std::move(stream));
ws_->set_option(websocket::stream_base::timeout::suggested(beast::role_type::client));
ws_->handshake(host, target);
// send initiation data
nlohmann::json init = {
{"type", "conversation_initiation_client_data"},
{"custom_llm_extra_body", nlohmann::json::object()},
{"conversation_config_override", nlohmann::json::object()},
{"dynamic_variables", nlohmann::json::object()}
};
ws_->write(boost::asio::buffer(init.dump()));
// Prepare audio callback
auto inputCb = [this](const std::vector<char>& audio) {
nlohmann::json msg = {
{"user_audio_chunk", base64Encode(audio)}
};
ws_->write(boost::asio::buffer(msg.dump()));
};
audioInterface_->start(inputCb);
beast::flat_buffer buffer;
while (!shouldStop_.load()) {
beast::error_code ec;
ws_->read(buffer, ec);
if (ec) {
std::cerr << "Websocket read error: " << ec.message() << std::endl;
break;
}
auto text = beast::buffers_to_string(buffer.data());
buffer.consume(buffer.size());
try {
auto message = nlohmann::json::parse(text);
handleMessage(message);
} catch (const std::exception& ex) {
std::cerr << "JSON parse error: " << ex.what() << std::endl;
}
}
} catch (const std::exception& ex) {
std::cerr << "Conversation error: " << ex.what() << std::endl;
}
}
void Conversation::handleMessage(const nlohmann::json& message) {
std::string type = message.value("type", "");
if (type == "conversation_initiation_metadata") {
conversationId_ = message["conversation_initiation_metadata_event"]["conversation_id"].get<std::string>();
} else if (type == "audio") {
auto event = message["audio_event"];
int eventId = std::stoi(toString(event["event_id"]));
if (eventId <= lastInterruptId_.load()) return;
auto audioBytes = base64Decode(event["audio_base_64"].get<std::string>());
audioInterface_->output(audioBytes);
} else if (type == "agent_response" && callbackAgentResponse_) {
auto event = message["agent_response_event"];
callbackAgentResponse_(event["agent_response"].get<std::string>());
} else if (type == "agent_response_correction" && callbackAgentResponseCorrection_) {
auto event = message["agent_response_correction_event"];
callbackAgentResponseCorrection_(event["original_agent_response"].get<std::string>(),
event["corrected_agent_response"].get<std::string>());
} else if (type == "user_transcript" && callbackUserTranscript_) {
auto event = message["user_transcription_event"];
callbackUserTranscript_(event["user_transcript"].get<std::string>());
} else if (type == "interruption") {
auto event = message["interruption_event"];
lastInterruptId_.store(std::stoi(toString(event["event_id"])));
audioInterface_->interrupt();
} else if (type == "ping") {
auto event = message["ping_event"];
nlohmann::json pong = {{"type", "pong"}, {"event_id", event["event_id"]}};
ws_->write(boost::asio::buffer(pong.dump()));
if (callbackLatencyMeasurement_ && event.contains("ping_ms")) {
int latency = event["ping_ms"].is_number() ? event["ping_ms"].get<int>() : std::stoi(event["ping_ms"].get<std::string>());
callbackLatencyMeasurement_(latency);
}
}
// Note: client tool call handling omitted for brevity.
}

View File

@@ -0,0 +1,131 @@
#include "DefaultAudioInterface.hpp"
#include <cstring>
#include <iostream>
DefaultAudioInterface::DefaultAudioInterface() {
PaError err = Pa_Initialize();
if (err != paNoError) {
throw std::runtime_error("PortAudio initialization failed");
}
}
DefaultAudioInterface::~DefaultAudioInterface() {
if (!shouldStop_.load()) {
stop();
}
Pa_Terminate();
}
void DefaultAudioInterface::start(AudioCallback inputCallback) {
inputCallback_ = std::move(inputCallback);
PaStreamParameters inputParams;
std::memset(&inputParams, 0, sizeof(inputParams));
inputParams.channelCount = 1;
inputParams.device = Pa_GetDefaultInputDevice();
inputParams.sampleFormat = paInt16;
inputParams.suggestedLatency = Pa_GetDeviceInfo(inputParams.device)->defaultLowInputLatency;
inputParams.hostApiSpecificStreamInfo = nullptr;
PaStreamParameters outputParams;
std::memset(&outputParams, 0, sizeof(outputParams));
outputParams.channelCount = 1;
outputParams.device = Pa_GetDefaultOutputDevice();
outputParams.sampleFormat = paInt16;
outputParams.suggestedLatency = Pa_GetDeviceInfo(outputParams.device)->defaultLowOutputLatency;
outputParams.hostApiSpecificStreamInfo = nullptr;
PaError err = Pa_OpenStream(&inputStream_, &inputParams, nullptr, 16000, INPUT_FRAMES_PER_BUFFER, paClipOff,
&DefaultAudioInterface::inputCallbackStatic, this);
if (err != paNoError) {
throw std::runtime_error("Failed to open input stream");
}
err = Pa_OpenStream(&outputStream_, nullptr, &outputParams, 16000, OUTPUT_FRAMES_PER_BUFFER, paClipOff, nullptr, nullptr);
if (err != paNoError) {
throw std::runtime_error("Failed to open output stream");
}
if ((err = Pa_StartStream(inputStream_)) != paNoError) {
throw std::runtime_error("Failed to start input stream");
}
if ((err = Pa_StartStream(outputStream_)) != paNoError) {
throw std::runtime_error("Failed to start output stream");
}
shouldStop_.store(false);
outputThread_ = std::thread(&DefaultAudioInterface::outputThreadFunc, this);
}
void DefaultAudioInterface::stop() {
shouldStop_.store(true);
queueCv_.notify_all();
if (outputThread_.joinable()) {
outputThread_.join();
}
if (inputStream_) {
Pa_StopStream(inputStream_);
Pa_CloseStream(inputStream_);
inputStream_ = nullptr;
}
if (outputStream_) {
Pa_StopStream(outputStream_);
Pa_CloseStream(outputStream_);
outputStream_ = nullptr;
}
}
void DefaultAudioInterface::output(const std::vector<char>& audio) {
{
std::lock_guard<std::mutex> lg(queueMutex_);
outputQueue_.emplace(audio);
}
queueCv_.notify_one();
}
void DefaultAudioInterface::interrupt() {
std::lock_guard<std::mutex> lg(queueMutex_);
std::queue<std::vector<char>> empty;
std::swap(outputQueue_, empty);
}
int DefaultAudioInterface::inputCallbackStatic(const void* input, void* /*output*/, unsigned long frameCount,
const PaStreamCallbackTimeInfo* /*timeInfo*/, PaStreamCallbackFlags /*statusFlags*/,
void* userData) {
auto* self = static_cast<DefaultAudioInterface*>(userData);
return self->inputCallbackInternal(input, frameCount);
}
int DefaultAudioInterface::inputCallbackInternal(const void* input, unsigned long frameCount) {
if (!input || !inputCallback_) {
return paContinue;
}
if (outputPlaying_.load()) {
// Suppress microphone input while playing output to avoid echo feedback.
return paContinue;
}
const size_t bytes = frameCount * sizeof(int16_t);
std::vector<char> buffer(bytes);
std::memcpy(buffer.data(), input, bytes);
inputCallback_(buffer);
return paContinue;
}
void DefaultAudioInterface::outputThreadFunc() {
while (!shouldStop_.load()) {
std::vector<char> audio;
{
std::unique_lock<std::mutex> lk(queueMutex_);
queueCv_.wait(lk, [this] { return shouldStop_.load() || !outputQueue_.empty(); });
if (shouldStop_.load()) break;
audio = std::move(outputQueue_.front());
outputQueue_.pop();
}
if (!audio.empty() && outputStream_) {
outputPlaying_.store(true);
Pa_WriteStream(outputStream_, audio.data(), audio.size() / sizeof(int16_t));
outputPlaying_.store(false);
}
}
}

View File

@@ -0,0 +1,31 @@
#include "Conversation.hpp"
#include "DefaultAudioInterface.hpp"
#include <cstdlib>
#include <iostream>
#include <memory>
int main() {
const char* agentIdEnv = std::getenv("AGENT_ID");
if (!agentIdEnv) {
std::cerr << "AGENT_ID environment variable must be set" << std::endl;
return 1;
}
std::string agentId(agentIdEnv);
auto audioInterface = std::make_shared<DefaultAudioInterface>();
Conversation conv(agentId, /*requiresAuth*/ false, audioInterface,
[](const std::string& resp) { std::cout << "Agent: " << resp << std::endl; },
[](const std::string& orig, const std::string& corrected) {
std::cout << "Agent correction: " << orig << " -> " << corrected << std::endl; },
[](const std::string& transcript) { std::cout << "User: " << transcript << std::endl; });
conv.startSession();
std::cout << "Press Enter to quit..." << std::endl;
std::cin.get();
conv.endSession();
auto convId = conv.waitForSessionEnd();
std::cout << "Conversation ID: " << convId << std::endl;
return 0;
}