aissia/src/shared/llm/OpenAIProvider.cpp
StillHammer bc3b6cbaba feat: Implement Phase 1 complete - All 6 core modules
## 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>
2025-11-26 00:42:18 +08:00

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