Skip to content

Commit d373369

Browse files
committed
feat: Add MCP tools integration and fix circular dependencies
- Add MCP tools support to LLMRequestConfig with OpenAI::ToolVariant - Move function implementations from headers to .cpp files to avoid circular dependencies - Fix LLMUsage redefinition by keeping single definition in OpenAITypes.h - Update OpenAI responses API to handle tools configuration - Enable structured output and tool calling for LLM requests
1 parent 7f287a0 commit d373369

File tree

4 files changed

+274
-233
lines changed

4 files changed

+274
-233
lines changed

include/core/LLMTypes.h

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
#pragma once
22
#include <functional>
3+
#include <memory>
34
#include <nlohmann/json.hpp>
45
#include <optional>
56
#include <string>
7+
#include <variant>
68
#include <vector>
79

10+
// Include OpenAITypes.h to get the full definition of ToolVariant
11+
#include "openai/OpenAITypes.h"
12+
813
using json = nlohmann::json;
914

1015
// Context type using standard C++ vectors of generic objects
@@ -20,6 +25,7 @@ struct LLMRequestConfig {
2025

2126
std::optional<float> temperature; // Optional temperature (filtered by model support)
2227
std::optional<int> maxTokens; // Optional max tokens
28+
std::optional<std::vector<OpenAI::ToolVariant>> tools; // Optional tools for function calling
2329

2430
// Convenience method for any model name
2531
void setModel(const std::string& modelName) { model = modelName; }
@@ -31,10 +37,12 @@ struct LLMRequestConfig {
3137
std::string toString() const {
3238
std::string schemaStr = schemaObject.has_value() ? schemaObject->dump() : jsonSchema;
3339
std::string tempStr = temperature.has_value() ? std::to_string(*temperature) : "not set";
40+
std::string toolsStr = tools.has_value() ? std::to_string(tools->size()) + " tools" : "no tools";
3441
return "LLMRequestConfig { client: " + client + ", model: " + getModelString() +
3542
", functionName: " + functionName + ", schema: " + schemaStr +
3643
", temperature: " + tempStr +
37-
", maxTokens: " + std::to_string(maxTokens.has_value() ? *maxTokens : 0) + " }";
44+
", maxTokens: " + std::to_string(maxTokens.has_value() ? *maxTokens : 0) +
45+
", tools: " + toolsStr + " }";
3846
}
3947
};
4048

@@ -79,18 +87,7 @@ struct LLMRequest {
7987
}
8088
};
8189

82-
struct LLMUsage {
83-
int inputTokens = 0;
84-
int outputTokens = 0;
85-
86-
int totalTokens() const { return inputTokens + outputTokens; }
87-
88-
std::string toString() const {
89-
return "LLMUsage { inputTokens: " + std::to_string(inputTokens) +
90-
", outputTokens: " + std::to_string(outputTokens) +
91-
", totalTokens: " + std::to_string(totalTokens()) + " }";
92-
}
93-
};
90+
// LLMUsage is now defined in OpenAITypes.h to avoid circular dependency
9491

9592
struct LLMResponse {
9693
json result = json::object();

include/openai/OpenAITypes.h

Lines changed: 22 additions & 220 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,23 @@
66
#include <variant>
77
#include <vector>
88

9-
#include "core/LLMTypes.h"
9+
// Forward declarations to avoid circular dependency
10+
struct LLMRequest;
11+
struct LLMResponse;
12+
13+
// Define LLMUsage here to avoid circular dependency
14+
struct LLMUsage {
15+
int inputTokens = 0;
16+
int outputTokens = 0;
17+
18+
int totalTokens() const { return inputTokens + outputTokens; }
19+
20+
std::string toString() const {
21+
return "LLMUsage { inputTokens: " + std::to_string(inputTokens) +
22+
", outputTokens: " + std::to_string(outputTokens) +
23+
", totalTokens: " + std::to_string(totalTokens()) + " }";
24+
}
25+
};
1026

1127
using json = nlohmann::json;
1228

@@ -1037,100 +1053,7 @@ const std::vector<std::string> CHAT_COMPLETION_MODELS = {"gpt-4", "gpt-4-turbo",
10371053
* These will be implemented in the source files
10381054
*/
10391055

1040-
// ResponsesRequest conversion methods
1041-
inline ResponsesRequest ResponsesRequest::fromLLMRequest(const LLMRequest& request) {
1042-
ResponsesRequest responsesReq;
1043-
responsesReq.model = request.config.model;
1044-
1045-
// Map prompt to OpenAI instructions field
1046-
if (!request.prompt.empty()) {
1047-
responsesReq.instructions = request.prompt;
1048-
}
1049-
1050-
// Map context to OpenAI inputValues
1051-
if (!request.context.empty()) {
1052-
// Convert context (vector of json) to InputMessages
1053-
std::vector<InputMessage> messages;
1054-
1055-
for (const auto& contextItem : request.context) {
1056-
// Case 1: Single JSON object with role/content
1057-
if (contextItem.is_object() && contextItem.contains("role") &&
1058-
contextItem.contains("content")) {
1059-
InputMessage msg;
1060-
msg.role = InputMessage::stringToRole(contextItem["role"].get<std::string>());
1061-
msg.content = contextItem["content"].get<std::string>();
1062-
messages.push_back(msg);
1063-
continue;
1064-
}
1065-
1066-
// Case 2: Array of message-like objects [{role, content}, ...]
1067-
if (contextItem.is_array()) {
1068-
for (const auto& item : contextItem) {
1069-
if (item.is_object() && item.contains("role") && item.contains("content")) {
1070-
InputMessage msg;
1071-
msg.role = InputMessage::stringToRole(item["role"].get<std::string>());
1072-
msg.content = item["content"].get<std::string>();
1073-
messages.push_back(msg);
1074-
}
1075-
}
1076-
continue;
1077-
}
1078-
1079-
// Fallback: stringify unknown item as a user message
1080-
InputMessage msg;
1081-
msg.role = InputMessage::Role::User;
1082-
msg.content = contextItem.dump();
1083-
messages.push_back(msg);
1084-
}
1085-
1086-
responsesReq.input = ResponsesInput::fromContentList(messages);
1087-
} else if (!request.prompt.empty()) {
1088-
// If context is empty but prompt is present, use prompt as input
1089-
responsesReq.input = ResponsesInput::fromText(request.prompt);
1090-
} else {
1091-
// If both context and prompt are empty, do not set input at all
1092-
responsesReq.input = std::nullopt;
1093-
}
1094-
responsesReq.toolChoice =
1095-
ToolChoiceMode::Auto; // Explicitly initialize to fix cppcheck warning
1096-
if (request.config.maxTokens.has_value() && *request.config.maxTokens > 0) {
1097-
responsesReq.maxOutputTokens = *request.config.maxTokens;
1098-
}
1099-
// Only set temperature if it's provided and valid
1100-
if (request.config.temperature.has_value() && *request.config.temperature >= 0.0f) {
1101-
responsesReq.temperature = static_cast<double>(*request.config.temperature);
1102-
}
1103-
if (!request.previousResponseId.empty()) {
1104-
responsesReq.previousResponseID = request.previousResponseId;
1105-
}
1106-
1107-
// Handle JSON schema for structured outputs
1108-
if (request.config.schemaObject.has_value()) {
1109-
// Use the structured schema object directly
1110-
const json& schemaJson = request.config.schemaObject.value();
1111-
// Use the function name as schema name (like aideas-core does)
1112-
std::string schemaName = request.config.functionName;
1113-
if (schemaName.empty()) {
1114-
schemaName = "response_schema";
1115-
}
1116-
responsesReq.text = TextOutputConfig(schemaName, schemaJson, true);
1117-
} else if (!request.config.jsonSchema.empty()) {
1118-
// Fallback to string schema for backward compatibility
1119-
try {
1120-
json schemaJson = json::parse(request.config.jsonSchema);
1121-
// Use the function name as schema name (like aideas-core does)
1122-
std::string schemaName = request.config.functionName;
1123-
if (schemaName.empty()) {
1124-
schemaName = "response_schema";
1125-
}
1126-
responsesReq.text = TextOutputConfig(schemaName, schemaJson, true);
1127-
} catch (const std::exception& e) {
1128-
throw std::runtime_error("Invalid JSON schema: " + std::string(e.what()));
1129-
}
1130-
}
1131-
1132-
return responsesReq;
1133-
}
1056+
// Conversion methods - implementations moved to .cpp file to avoid circular dependency
11341057

11351058
inline ResponsesRequest ResponsesRequest::fromJson(const json& j) {
11361059
ResponsesRequest req;
@@ -1246,57 +1169,7 @@ inline ResponsesResponse ResponsesResponse::fromJson(const json& j) {
12461169
return resp;
12471170
}
12481171

1249-
inline LLMResponse ResponsesResponse::toLLMResponse(bool expectStructuredOutput) const {
1250-
LLMResponse llmResp;
1251-
llmResp.success = (status == ResponseStatus::Completed);
1252-
llmResp.responseId = id;
1253-
llmResp.usage = usage;
1254-
1255-
if (hasError()) {
1256-
llmResp.errorMessage = error->dump();
1257-
} else {
1258-
// Extract text output from the response
1259-
std::string textOutput = getOutputText();
1260-
if (!textOutput.empty()) {
1261-
if (expectStructuredOutput) {
1262-
// Parse as JSON for structured output
1263-
llmResp.result = json::parse(textOutput);
1264-
} else {
1265-
// Wrap free-form text in text field
1266-
llmResp.result = json{{"text", textOutput}};
1267-
}
1268-
} else {
1269-
llmResp.result = json::object();
1270-
}
1271-
1272-
// Add function calls if any
1273-
auto functionCalls = getFunctionCalls();
1274-
if (!functionCalls.empty()) {
1275-
json calls = json::array();
1276-
std::transform(functionCalls.begin(), functionCalls.end(), std::back_inserter(calls),
1277-
[](const FunctionCall& call) {
1278-
return json{{"id", call.id},
1279-
{"name", call.name},
1280-
{"arguments", call.arguments}};
1281-
});
1282-
llmResp.result["function_calls"] = calls;
1283-
}
1284-
1285-
// Add images if any
1286-
auto images = getImageGenerations();
1287-
if (!images.empty()) {
1288-
json imageArray = json::array();
1289-
for (const auto& img : images) {
1290-
if (img.result) {
1291-
imageArray.push_back(*img.result);
1292-
}
1293-
}
1294-
llmResp.result["images"] = imageArray;
1295-
}
1296-
}
1297-
1298-
return llmResp;
1299-
}
1172+
// Implementation moved to .cpp file
13001173

13011174
// Convenience methods for ResponsesResponse
13021175
inline std::string ResponsesResponse::getOutputText() const {
@@ -1338,49 +1211,7 @@ inline std::vector<ImageGenerationCall> ResponsesResponse::getImageGenerations()
13381211
}
13391212

13401213
// ChatCompletionRequest conversion methods
1341-
inline ChatCompletionRequest ChatCompletionRequest::fromLLMRequest(const LLMRequest& request) {
1342-
ChatCompletionRequest chatReq;
1343-
chatReq.model = request.config.model;
1344-
1345-
// Convert prompt to messages
1346-
if (!request.prompt.empty()) {
1347-
ChatMessage userMsg;
1348-
userMsg.role = "user";
1349-
userMsg.content = request.prompt;
1350-
chatReq.messages.push_back(userMsg);
1351-
}
1352-
1353-
if (request.config.maxTokens.has_value() && *request.config.maxTokens > 0) {
1354-
chatReq.maxTokens = *request.config.maxTokens;
1355-
}
1356-
// Only set temperature if it's provided and valid
1357-
if (request.config.temperature.has_value() && *request.config.temperature >= 0.0f) {
1358-
chatReq.temperature = static_cast<double>(*request.config.temperature);
1359-
}
1360-
1361-
return chatReq;
1362-
}
1363-
1364-
inline LLMRequest ChatCompletionRequest::toLLMRequest() const {
1365-
LLMRequestConfig config;
1366-
config.client = "openai";
1367-
config.model = model;
1368-
if (temperature) config.temperature = static_cast<float>(*temperature);
1369-
if (maxTokens) config.maxTokens = *maxTokens;
1370-
1371-
std::string prompt;
1372-
if (!messages.empty()) {
1373-
// Use the last user message as prompt
1374-
for (auto it = messages.rbegin(); it != messages.rend(); ++it) {
1375-
if (it->role == "user") {
1376-
prompt = it->content;
1377-
break;
1378-
}
1379-
}
1380-
}
1381-
1382-
return LLMRequest(config, prompt);
1383-
}
1214+
// Implementation moved to .cpp file
13841215

13851216
// ChatCompletionResponse conversion methods
13861217
inline ChatCompletionResponse ChatCompletionResponse::fromJson(const json& j) {
@@ -1411,41 +1242,12 @@ inline ChatCompletionResponse ChatCompletionResponse::fromJson(const json& j) {
14111242
return resp;
14121243
}
14131244

1414-
inline LLMResponse ChatCompletionResponse::toLLMResponse(bool expectStructuredOutput) const {
1415-
LLMResponse llmResp;
1416-
llmResp.success = !choices.empty();
1417-
llmResp.responseId = id;
1418-
llmResp.usage = usage;
1419-
1420-
if (!choices.empty()) {
1421-
llmResp.result = json::object();
1422-
llmResp.result["text"] = choices[0].message.content;
1423-
if (choices[0].message.toolCalls) {
1424-
llmResp.result["tool_calls"] = *choices[0].message.toolCalls;
1425-
}
1426-
} else {
1427-
llmResp.errorMessage = "No choices returned";
1428-
}
1429-
1430-
return llmResp;
1431-
}
1245+
// Implementation moved to .cpp file
14321246

14331247
/**
14341248
* Utility functions implementation
14351249
*/
1436-
inline ApiType detectApiType(const LLMRequest& request) {
1437-
const std::string& model = request.config.model;
1438-
1439-
// Check if it's a Responses API model
1440-
if (std::any_of(
1441-
RESPONSES_MODELS.begin(), RESPONSES_MODELS.end(),
1442-
[&model](const std::string& responsesModel) { return model == responsesModel; })) {
1443-
return ApiType::RESPONSES;
1444-
}
1445-
1446-
// Default to Chat Completions for most models
1447-
return ApiType::CHAT_COMPLETIONS;
1448-
}
1250+
// Implementation moved to .cpp file to avoid circular dependency
14491251

14501252
inline bool supportsResponses(const std::string& model) {
14511253
return std::any_of(

src/openai/OpenAIResponsesApi.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
#include <stdexcept>
66

77
#include "openai/OpenAIHttpClient.h"
8+
#include "core/LLMTypes.h" // Include for complete type definitions
89

910
OpenAIResponsesApi::OpenAIResponsesApi(std::shared_ptr<OpenAIHttpClient> httpClient)
1011
: httpClient_(std::move(httpClient)) {}
1112

13+
1214
// Core Responses API methods
1315
OpenAI::ResponsesResponse OpenAIResponsesApi::create(const OpenAI::ResponsesRequest& request) {
1416
try {

0 commit comments

Comments
 (0)