## New Modules - StorageModule: SQLite persistence for sessions, app usage, conversations - MonitoringModule: Cross-platform window tracking (Win32/X11) - AIModule: Multi-provider LLM integration with agentic tool loop - VoiceModule: TTS/STT coordination with speak queue ## Shared Libraries - AissiaLLM: ILLMProvider abstraction (Claude + OpenAI providers) - AissiaPlatform: IWindowTracker abstraction (Win32 + X11) - AissiaAudio: ITTSEngine (SAPI/espeak) + ISTTEngine (Whisper API) - HttpClient: Header-only HTTP client with OpenSSL ## Configuration - Added JSON configs for all modules (storage, monitoring, ai, voice) - Multi-provider LLM config with Claude and OpenAI support ## Dependencies - SQLite3, OpenSSL, cpp-httplib (FetchContent) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
202 lines
6.5 KiB
C++
202 lines
6.5 KiB
C++
#include "OpenAIProvider.hpp"
|
|
#include <spdlog/sinks/stdout_color_sinks.h>
|
|
#include <cstdlib>
|
|
|
|
namespace aissia {
|
|
|
|
OpenAIProvider::OpenAIProvider(const json& config) {
|
|
m_logger = spdlog::get("OpenAIProvider");
|
|
if (!m_logger) {
|
|
m_logger = spdlog::stdout_color_mt("OpenAIProvider");
|
|
}
|
|
|
|
// Get API key from environment
|
|
std::string apiKeyEnv = config.value("api_key_env", "OPENAI_API_KEY");
|
|
const char* apiKey = std::getenv(apiKeyEnv.c_str());
|
|
if (!apiKey) {
|
|
m_logger->error("API key not found in environment: {}", apiKeyEnv);
|
|
throw std::runtime_error("Missing API key: " + apiKeyEnv);
|
|
}
|
|
m_apiKey = apiKey;
|
|
|
|
m_model = config.value("model", "gpt-4o");
|
|
m_maxTokens = config.value("max_tokens", 4096);
|
|
m_baseUrl = config.value("base_url", "https://api.openai.com");
|
|
|
|
m_client = std::make_unique<HttpClient>(m_baseUrl, 60);
|
|
m_client->setBearerToken(m_apiKey);
|
|
m_client->setHeader("Content-Type", "application/json");
|
|
|
|
m_logger->info("OpenAIProvider initialized: model={}", m_model);
|
|
}
|
|
|
|
LLMResponse OpenAIProvider::chat(const std::string& systemPrompt,
|
|
const json& messages,
|
|
const json& tools) {
|
|
// Build messages array with system prompt
|
|
json allMessages = json::array();
|
|
allMessages.push_back({
|
|
{"role", "system"},
|
|
{"content", systemPrompt}
|
|
});
|
|
for (const auto& msg : messages) {
|
|
allMessages.push_back(msg);
|
|
}
|
|
|
|
json request = {
|
|
{"model", m_model},
|
|
{"max_tokens", m_maxTokens},
|
|
{"messages", allMessages}
|
|
};
|
|
|
|
if (!tools.empty()) {
|
|
request["tools"] = convertTools(tools);
|
|
}
|
|
|
|
m_logger->debug("Sending request to OpenAI: {} messages", allMessages.size());
|
|
|
|
auto response = m_client->post("/v1/chat/completions", request);
|
|
|
|
if (!response.success) {
|
|
m_logger->error("OpenAI API error: {}", response.error);
|
|
LLMResponse errorResp;
|
|
errorResp.text = "Error: " + response.error;
|
|
errorResp.is_end_turn = true;
|
|
return errorResp;
|
|
}
|
|
|
|
try {
|
|
json jsonResponse = json::parse(response.body);
|
|
m_lastRawResponse = jsonResponse;
|
|
return parseResponse(jsonResponse);
|
|
} catch (const json::exception& e) {
|
|
m_logger->error("Failed to parse OpenAI response: {}", e.what());
|
|
LLMResponse errorResp;
|
|
errorResp.text = "Parse error: " + std::string(e.what());
|
|
errorResp.is_end_turn = true;
|
|
return errorResp;
|
|
}
|
|
}
|
|
|
|
json OpenAIProvider::convertTools(const json& tools) {
|
|
// Convert to OpenAI function format
|
|
json openaiTools = json::array();
|
|
|
|
for (const auto& tool : tools) {
|
|
json openaiTool = {
|
|
{"type", "function"},
|
|
{"function", {
|
|
{"name", tool.value("name", "")},
|
|
{"description", tool.value("description", "")},
|
|
{"parameters", tool.value("input_schema", json::object())}
|
|
}}
|
|
};
|
|
openaiTools.push_back(openaiTool);
|
|
}
|
|
|
|
return openaiTools;
|
|
}
|
|
|
|
LLMResponse OpenAIProvider::parseResponse(const json& response) {
|
|
LLMResponse result;
|
|
|
|
// Parse usage
|
|
if (response.contains("usage")) {
|
|
result.input_tokens = response["usage"].value("prompt_tokens", 0);
|
|
result.output_tokens = response["usage"].value("completion_tokens", 0);
|
|
}
|
|
|
|
result.model = response.value("model", m_model);
|
|
|
|
// Parse choices
|
|
if (response.contains("choices") && !response["choices"].empty()) {
|
|
const auto& choice = response["choices"][0];
|
|
result.stop_reason = choice.value("finish_reason", "");
|
|
|
|
if (choice.contains("message")) {
|
|
const auto& message = choice["message"];
|
|
|
|
// Get text content
|
|
if (message.contains("content") && !message["content"].is_null()) {
|
|
result.text = message["content"].get<std::string>();
|
|
}
|
|
|
|
// Get tool calls
|
|
if (message.contains("tool_calls") && message["tool_calls"].is_array()) {
|
|
for (const auto& tc : message["tool_calls"]) {
|
|
ToolCall call;
|
|
call.id = tc.value("id", "");
|
|
if (tc.contains("function")) {
|
|
call.name = tc["function"].value("name", "");
|
|
std::string args = tc["function"].value("arguments", "{}");
|
|
try {
|
|
call.input = json::parse(args);
|
|
} catch (...) {
|
|
call.input = json::object();
|
|
}
|
|
}
|
|
result.tool_calls.push_back(call);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Determine if this is end turn
|
|
result.is_end_turn = result.tool_calls.empty() ||
|
|
result.stop_reason == "stop";
|
|
|
|
m_logger->debug("OpenAI response: text={} chars, tools={}, stop={}",
|
|
result.text.size(), result.tool_calls.size(), result.stop_reason);
|
|
|
|
return result;
|
|
}
|
|
|
|
json OpenAIProvider::formatToolResults(const std::vector<ToolResult>& results) {
|
|
// OpenAI format: separate messages with role "tool"
|
|
json messages = json::array();
|
|
|
|
for (const auto& result : results) {
|
|
messages.push_back({
|
|
{"role", "tool"},
|
|
{"tool_call_id", result.tool_call_id},
|
|
{"content", result.content}
|
|
});
|
|
}
|
|
|
|
// Return array of messages (caller should append each)
|
|
return messages;
|
|
}
|
|
|
|
void OpenAIProvider::appendAssistantMessage(json& messages, const LLMResponse& response) {
|
|
// Build assistant message
|
|
json assistantMsg = {
|
|
{"role", "assistant"}
|
|
};
|
|
|
|
if (!response.text.empty()) {
|
|
assistantMsg["content"] = response.text;
|
|
} else {
|
|
assistantMsg["content"] = nullptr;
|
|
}
|
|
|
|
// Add tool_calls if present
|
|
if (!response.tool_calls.empty()) {
|
|
json toolCalls = json::array();
|
|
for (const auto& call : response.tool_calls) {
|
|
toolCalls.push_back({
|
|
{"id", call.id},
|
|
{"type", "function"},
|
|
{"function", {
|
|
{"name", call.name},
|
|
{"arguments", call.input.dump()}
|
|
}}
|
|
});
|
|
}
|
|
assistantMsg["tool_calls"] = toolCalls;
|
|
}
|
|
|
|
messages.push_back(assistantMsg);
|
|
}
|
|
|
|
} // namespace aissia
|