Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
f153d7f
feat: AI is now proactive during the interview
Andriamanampisoa Jul 14, 2026
22d2383
feat: Setup the structure of the interview (greatings, experience...)
Andriamanampisoa Jul 14, 2026
70171ba
feat: relay session_start to STS for the initial handshake
Andriamanampisoa Jul 14, 2026
a89c168
feat: Automatic AI startup, end of conversation, and session resumption
Andriamanampisoa Jul 14, 2026
317ed11
merge: fix conflicts between the branch 200 and staging
Andriamanampisoa Jul 14, 2026
31f1c73
build: fix timeout while installing the dependencies
Andriamanampisoa Jul 14, 2026
b9b9925
tests: add tests for the sts pipeline
Andriamanampisoa Jul 14, 2026
dc6cd2a
fix: the duplicate session_start hang
Andriamanampisoa Jul 14, 2026
04a65cf
fix: the early PARCOURS transition
Andriamanampisoa Jul 14, 2026
3252fbe
fix: the flow state revival after clear
Andriamanampisoa Jul 14, 2026
f5fb2e2
Merge branch 'staging' of github.com:Tugduoff/TalkUp.AI into 200-fix-…
Andriamanampisoa Jul 14, 2026
6b5ce09
Merge branch 'staging' into 200-fix-the-structure-of-the-interview
Andriamanampisoa Jul 14, 2026
36e1b9b
fix: Include simulation_complete in the success payload when the pipe…
Andriamanampisoa Jul 14, 2026
0f1ad9f
fix: reset greetingSentRef in onClose so reconnects trigger another s…
Andriamanampisoa Jul 14, 2026
64be199
Merge branch '200-fix-the-structure-of-the-interview' of github.com:T…
Andriamanampisoa Jul 14, 2026
acb33eb
fix: address PR #170 review - greeting replay on reconnect, keyword s…
BhuvanArn Jul 14, 2026
e723493
Merge branch 'staging' into 200-fix-the-structure-of-the-interview
Andriamanampisoa Jul 14, 2026
0f2cd21
fix: session start drops VA followups
Andriamanampisoa Jul 14, 2026
3845012
fix: restore failure cancels interview
Andriamanampisoa Jul 14, 2026
e7024c0
refactor: changes required for the pr
Andriamanampisoa Jul 14, 2026
a0842ad
fix: defer interview completion until farewell audio finishes; format…
BhuvanArn Jul 14, 2026
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
43 changes: 35 additions & 8 deletions ai/core/server/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
FROM debian:bullseye-slim AS builder

RUN apt-get update && apt-get install -y \
g++ cmake git curl libcurl4-openssl-dev wget libboost-all-dev libssl-dev && \
apt-get clean && rm -rf /var/lib/apt/lists/*

RUN wget https://github.com/Kitware/CMake/releases/download/v3.31.6/cmake-3.31.6-linux-x86_64.tar.gz && \
# Reduce apt failures on slow/unstable networks (mirrors, large metapackages).
RUN printf '%s\n' \
'Acquire::Retries "5";' \
'Acquire::http::Timeout "120";' \
'Acquire::https::Timeout "120";' \
> /etc/apt/apt.conf.d/80-retries

RUN apt-get update && apt-get install -y --no-install-recommends \
g++ \
make \
git \
curl \
wget \
ca-certificates \
libcurl4-openssl-dev \
libssl-dev \
libboost-system-dev \
libboost-date-time-dev \
libboost1.74-dev \
&& apt-get clean && rm -rf /var/lib/apt/lists/*

RUN wget -q https://github.com/Kitware/CMake/releases/download/v3.31.6/cmake-3.31.6-linux-x86_64.tar.gz && \
tar -xvzf cmake-3.31.6-linux-x86_64.tar.gz --strip-components=1 -C /usr/local && \
rm cmake-3.31.6-linux-x86_64.tar.gz

Expand All @@ -15,9 +32,19 @@ RUN rm -rf build && cmake -B build -S . && cmake --build build

FROM debian:bullseye-slim

RUN apt-get update && apt-get install -y \
libcurl4-openssl-dev libssl-dev && \
apt-get clean && rm -rf /var/lib/apt/lists/*
RUN printf '%s\n' \
'Acquire::Retries "5";' \
'Acquire::http::Timeout "120";' \
'Acquire::https::Timeout "120";' \
> /etc/apt/apt.conf.d/80-retries

RUN apt-get update && apt-get install -y --no-install-recommends \
libcurl4 \
libssl1.1 \
libboost-system1.74.0 \
libboost-date-time1.74.0 \
ca-certificates \
&& apt-get clean && rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY --from=builder /app/build/talkup_ai_mic_server /app/talkup_ai_mic_server
Expand Down
14 changes: 14 additions & 0 deletions ai/core/server/inc/network/MicroservicesManager.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,14 @@ namespace talkup_network {
*/
static void send_session_end_to_sts(const std::string &interview_id);

/**
* @brief Request STS to generate the proactive opening greeting.
*/
static void send_session_start_to_sts(
const nlohmann::json &data,
ResponseCallback callback,
const std::shared_ptr<WsClientSession> &client_session = nullptr);

/**
* @brief Initialize WebSocket connections to all registered microservices.
* It's establishes persistent WebSocket connections to each microservice
Expand Down Expand Up @@ -136,6 +144,7 @@ namespace talkup_network {
enum class StsJobKind {
StreamChunk,
SimulationContext,
SessionStart,
};

struct StsJob {
Expand Down Expand Up @@ -207,6 +216,11 @@ namespace talkup_network {
ResponseCallback callback,
const std::weak_ptr<WsClientSession> &client_session = {});

static void process_session_start_job(
const nlohmann::json &data,
ResponseCallback callback,
const std::weak_ptr<WsClientSession> &client_session = {});

static void process_simulation_context_job(
const std::string &interview_id,
const nlohmann::json &context_data,
Expand Down
3 changes: 3 additions & 0 deletions ai/core/server/inc/network/WebsocketManager.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ namespace talkup_network {
void handle_session_end(const nlohmann::json& json, crow::websocket::connection& conn,
std::shared_ptr<MicroservicesManager> microservices_manager);

void handle_session_start(const nlohmann::json& json, crow::websocket::connection& conn,
std::shared_ptr<MicroservicesManager> microservices_manager);

private:
std::unordered_map<std::string, std::function<void(const nlohmann::json&,
crow::websocket::connection&, std::shared_ptr<MicroservicesManager>)>> _type_handlers;
Expand Down
155 changes: 155 additions & 0 deletions ai/core/server/src/network/MicroservicesManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,8 @@ void talkup_network::MicroservicesManager::create_service_worker(
job.interview_id,
job.context_data,
job.context_promise);
} else if (job.kind == WebSocketConnection::StsJobKind::SessionStart) {
process_session_start_job(job.data, std::move(job.callback), job.client_session);
} else {
process_sts_job(job.data, std::move(job.callback), job.client_session);
}
Expand Down Expand Up @@ -497,6 +499,42 @@ void talkup_network::MicroservicesManager::send_to_sts_microservice(
}
}

void talkup_network::MicroservicesManager::send_session_start_to_sts(
const nlohmann::json &data,
ResponseCallback callback,
const std::shared_ptr<WsClientSession> &client_session)
{
nlohmann::json err_response;
bool enqueue_ok = false;

{
std::lock_guard<std::mutex> lock(__ws_mutex);
auto it = __ws_connections.find("sts");
if (it == __ws_connections.end() || !it->second.is_connected) {
std::cerr << "[MicroservicesManager] STS connection not available" << std::endl;
err_response = {{"error", "STS connection not available"}};
} else {
{
std::lock_guard<std::mutex> qlock(it->second.queue_mutex);
WebSocketConnection::StsJob job;
job.kind = WebSocketConnection::StsJobKind::SessionStart;
job.data = data;
job.callback = std::move(callback);
if (client_session)
job.client_session = client_session;
it->second.job_queue.push(std::move(job));
}
it->second.queue_cv.notify_one();
enqueue_ok = true;
std::cout << "[MicroservicesManager] Enqueued STS session_start job" << std::endl;
}
}

if (!enqueue_ok && callback) {
callback(err_response);
}
}

void talkup_network::MicroservicesManager::send_session_end_to_sts(
const std::string &interview_id)
{
Expand Down Expand Up @@ -864,6 +902,121 @@ void talkup_network::MicroservicesManager::process_sts_job(
}
}

void talkup_network::MicroservicesManager::process_session_start_job(
const nlohmann::json &data,
ResponseCallback callback,
const std::weak_ptr<WsClientSession> &client_session)
{
try {
std::shared_ptr<boost::beast::websocket::stream<boost::beast::tcp_stream>> ws;
std::mutex *io_mutex = nullptr;

{
std::lock_guard<std::mutex> lock(__ws_mutex);
auto it = __ws_connections.find("sts");
if (it == __ws_connections.end() || !it->second.is_connected ||
!it->second.ws || !it->second.ws->is_open()) {
if (it != __ws_connections.end())
it->second.is_connected = false;
} else {
ws = it->second.ws;
io_mutex = &it->second.io_mutex;
}
}

if (!ws || !io_mutex) {
if (!reconnect_service_connection("sts")) {
if (callback)
callback(nlohmann::json{{"error", "STS connection not available"}});
return;
}
std::lock_guard<std::mutex> lock(__ws_mutex);
auto it = __ws_connections.find("sts");
if (it == __ws_connections.end() || !it->second.ws) {
if (callback)
callback(nlohmann::json{{"error", "STS connection not available"}});
return;
}
ws = it->second.ws;
io_mutex = &it->second.io_mutex;
}

std::unique_lock<std::mutex> io_lock(*io_mutex);
const uint64_t request_id = ++g_sts_request_id;
std::string interview_id;
if (data.contains("stream_id") && data["stream_id"].is_string())
interview_id = data["stream_id"].get<std::string>();
if (data.contains("interview_id") && data["interview_id"].is_string())
interview_id = data["interview_id"].get<std::string>();

nlohmann::json start_json = {
{"services", {"STS"}},
{"type", "session_start"},
{"request_id", request_id},
{"timestamp", std::time(nullptr)},
{"data", nlohmann::json::object()},
};
if (!interview_id.empty()) {
start_json["interview_id"] = interview_id;
start_json["stream_id"] = interview_id;
}
ws->write(boost::asio::buffer(start_json.dump()));
std::cout << "[MicroservicesManager] Sent STS session_start request_id=" << request_id << std::endl;

const int timeout_ms = 120000;
const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeout_ms);

while (std::chrono::steady_clock::now() < deadline) {
const int remaining_ms = static_cast<int>(std::chrono::duration_cast<std::chrono::milliseconds>(
deadline - std::chrono::steady_clock::now()).count());
if (remaining_ms <= 0)
break;

nlohmann::json msg_json;
if (!read_sts_json_message(*ws, msg_json, std::min(remaining_ms, 30000)))
continue;

const std::string msg_type = msg_json.value("type", "");
if (msg_type == "pong" || msg_type == "simulation_context_ack")
continue;

if (try_dispatch_sts_va_followup(msg_json))
continue;

if (!msg_json.contains("request_id"))
continue;

const uint64_t response_id = msg_json.value("request_id", static_cast<uint64_t>(0));
if (response_id != request_id) {
if (try_dispatch_sts_va_followup(msg_json))
continue;
continue;
Comment thread
Andriamanampisoa marked this conversation as resolved.
}

if (msg_type == "sts_result") {
if (callback)
callback(msg_json);
io_lock.unlock();
return;
}

io_lock.unlock();
if (callback)
callback(msg_json);
return;
}

std::cerr << "[MicroservicesManager] session_start read timed out" << std::endl;
io_lock.unlock();
if (callback)
callback(nlohmann::json{{"type", "error"}, {"error", "Read timeout"}});
} catch (const std::exception &e) {
std::cerr << "[MicroservicesManager] session_start exception: " << e.what() << std::endl;
if (callback)
callback(nlohmann::json{{"type", "error"}, {"error", std::string("Exception: ") + e.what()}});
}
}

void talkup_network::MicroservicesManager::start_service_worker(const std::string &service_name)
{
std::lock_guard<std::mutex> lock(__ws_mutex);
Expand Down Expand Up @@ -908,6 +1061,8 @@ void talkup_network::MicroservicesManager::start_service_worker(const std::strin
job.interview_id,
job.context_data,
job.context_promise);
} else if (job.kind == WebSocketConnection::StsJobKind::SessionStart) {
process_session_start_job(job.data, std::move(job.callback), job.client_session);
} else {
process_sts_job(job.data, std::move(job.callback), job.client_session);
}
Expand Down
73 changes: 73 additions & 0 deletions ai/core/server/src/network/WebsocketManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ talkup_network::WsManager::WsManager()
crow::websocket::connection& conn, std::shared_ptr<MicroservicesManager> microservices_manager) {
handle_session_end(json, conn, microservices_manager);
};
_type_handlers["session_start"] = [this](const nlohmann::json& json,
crow::websocket::connection& conn, std::shared_ptr<MicroservicesManager> microservices_manager) {
handle_session_start(json, conn, microservices_manager);
};
}

void talkup_network::WsManager::connection_type_manager(nlohmann::json &json, crow::websocket::connection &conn,
Expand Down Expand Up @@ -316,6 +320,75 @@ void talkup_network::WsManager::handle_simulation_context(const nlohmann::json&
}).dump());
}

void talkup_network::WsManager::handle_session_start(const nlohmann::json& json,
crow::websocket::connection& conn, std::shared_ptr<MicroservicesManager> microservices_manager)
{
const std::string key = json.value("key", "");
const int64_t timestamp = json.value("timestamp", static_cast<int64_t>(
std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now().time_since_epoch()).count()));

std::string interview_id = json.value("stream_id", "");
if (json.contains("interview_id") && json["interview_id"].is_string())
interview_id = json["interview_id"].get<std::string>();

if (interview_id.empty()) {
conn.send_text(set_respond_json_format({
.type = "error",
.key = key,
.stream_id = "",
.format = "text",
.timestamp = timestamp,
.data = "session_start requires stream_id or interview_id"
}).dump());
return;
}

if (!microservices_manager) {
conn.send_text(set_respond_json_format({
.type = "error",
.key = key,
.stream_id = interview_id,
.format = "text",
.timestamp = timestamp,
.data = "microservices manager unavailable"
}).dump());
return;
}

auto client_session = WsClientSession::bind(conn);
microservices_manager->send_session_start_to_sts(json,
[this, client_session, key, interview_id](const nlohmann::json& sts_resp) {
if (!client_session->is_open())
return;

const std::string sts_type = sts_resp.value("type", "");
if (sts_resp.contains("error") || sts_type == "error" || sts_type == "warning") {
client_session->send_text(set_respond_json_format({
.type = "error",
.key = key,
.stream_id = interview_id,
.format = "text",
.timestamp = std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now().time_since_epoch()).count(),
.data = sts_resp.dump()
}).dump());
return;
}

client_session->send_text(set_respond_json_format({
.type = "sts_result",
.key = key,
.stream_id = interview_id,
.format = "audio",
.timestamp = std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now().time_since_epoch()).count(),
.data = sts_resp.dump()
}).dump());
},
client_session);
}

void talkup_network::WsManager::handle_session_end(const nlohmann::json& json,
crow::websocket::connection& conn, std::shared_ptr<MicroservicesManager> microservices_manager)
{
Expand Down
2 changes: 1 addition & 1 deletion ai/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ services:
- BACKEND_URL=${BACKEND_URL:-http://server:3000/v1/api}
- SIM_INTERNAL_API_KEY=${SIM_INTERNAL_API_KEY:-talkup-dev-internal-key}
- LLM_BACKEND=${LLM_BACKEND:-openrouter}
- LLM_MAX_NEW_TOKENS=${LLM_MAX_NEW_TOKENS:-160}
- LLM_MAX_NEW_TOKENS=${LLM_MAX_NEW_TOKENS:-320}
- VA_SERVICE_URL=http://verbal_analyzer:8006
- VA_ENABLED=true
- VA_TIMEOUT_SEC=3
Expand Down
11 changes: 10 additions & 1 deletion ai/microservices/behavior-analyzer/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
FROM python:3.10-slim

WORKDIR /app

ENV PIP_DEFAULT_TIMEOUT=120 \
PIP_RETRIES=5

COPY requirements.txt .
RUN pip install --upgrade pip && \
pip install --no-cache-dir -r requirements.txt

COPY . .
RUN pip install -r requirements.txt

CMD ["python", "main.py"]
Loading
Loading