Skip to content
This repository was archived by the owner on Jul 4, 2025. It is now read-only.

Commit 7d6199d

Browse files
committed
feat: add messages api
1 parent 5eda212 commit 7d6199d

31 files changed

+3127
-4
lines changed

engine/CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ else()
169169
endif()
170170

171171
aux_source_directory(controllers CTL_SRC)
172+
aux_source_directory(repositories REPO_SRC)
172173
aux_source_directory(services SERVICES_SRC)
173174
aux_source_directory(common COMMON_SRC)
174175
aux_source_directory(models MODEL_SRC)
@@ -180,7 +181,7 @@ aux_source_directory(utils UTILS_SRC)
180181

181182
target_include_directories(${TARGET_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} )
182183

183-
target_sources(${TARGET_NAME} PRIVATE ${UTILS_SRC} ${CONFIG_SRC} ${CTL_SRC} ${COMMON_SRC} ${SERVICES_SRC} ${DB_SRC} ${MIGR_SRC})
184+
target_sources(${TARGET_NAME} PRIVATE ${UTILS_SRC} ${CONFIG_SRC} ${CTL_SRC} ${COMMON_SRC} ${SERVICES_SRC} ${DB_SRC} ${MIGR_SRC} ${REPO_SRC})
184185

185186
set_target_properties(${TARGET_NAME} PROPERTIES
186187
RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_BINARY_DIR}

engine/cli/commands/engine_install_cmd.cc

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,6 @@ bool EngineInstallCmd::Exec(const std::string& engine,
179179
auto response = curl_utils::SimplePostJson(install_url.ToFullPath(),
180180
body.toStyledString());
181181
if (response.has_error()) {
182-
// TODO: namh refactor later
183182
Json::Value root;
184183
Json::Reader reader;
185184
if (!reader.parse(response.error(), root)) {
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#pragma once
2+
3+
#include "common/json_serializable.h"
4+
5+
namespace api_response {
6+
struct DeleteMessageResponse : JsonSerializable {
7+
std::string id;
8+
std::string object;
9+
bool deleted;
10+
11+
cpp::result<Json::Value, std::string> ToJson() override {
12+
Json::Value json;
13+
json["id"] = id;
14+
json["object"] = object;
15+
json["deleted"] = deleted;
16+
return json;
17+
}
18+
};
19+
} // namespace api_response

engine/common/json_serializable.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#pragma once
2+
3+
#include <json/value.h>
4+
#include "utils/result.hpp"
5+
6+
struct JsonSerializable {
7+
8+
virtual cpp::result<Json::Value, std::string> ToJson() = 0;
9+
10+
virtual ~JsonSerializable() = default;
11+
};

engine/common/message.h

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
#pragma once
2+
3+
#include <json/reader.h>
4+
#include <json/value.h>
5+
#include <json/writer.h>
6+
#include <cstdint>
7+
#include <string>
8+
#include "common/message_attachment.h"
9+
#include "common/message_attachment_factory.h"
10+
#include "common/message_content.h"
11+
#include "common/message_content_factory.h"
12+
#include "common/message_incomplete_detail.h"
13+
#include "common/message_role.h"
14+
#include "common/message_status.h"
15+
#include "common/variant_map.h"
16+
#include "json_serializable.h"
17+
#include "utils/logging_utils.h"
18+
#include "utils/result.hpp"
19+
20+
namespace ThreadMessage {
21+
22+
// Represents a message within a thread.
23+
struct Message : JsonSerializable {
24+
Message() = default;
25+
26+
Message(Message&&) = default;
27+
28+
Message& operator=(Message&&) = default;
29+
30+
Message(const Message&) = delete;
31+
32+
Message& operator=(const Message&) = delete;
33+
34+
// The identifier, which can be referenced in API endpoints.
35+
std::string id;
36+
37+
// The object type, which is always thread.message.
38+
std::string object = "thread.message";
39+
40+
// The Unix timestamp (in seconds) for when the message was created.
41+
uint32_t created_at;
42+
43+
// The thread ID that this message belongs to.
44+
std::string thread_id;
45+
46+
// The status of the message, which can be either in_progress, incomplete, or completed.
47+
Status status;
48+
49+
// On an incomplete message, details about why the message is incomplete.
50+
std::optional<IncompleteDetail> incomplete_details;
51+
52+
// The Unix timestamp (in seconds) for when the message was completed.
53+
std::optional<uint32_t> completed_at;
54+
55+
// The Unix timestamp (in seconds) for when the message was marked as incomplete.
56+
std::optional<uint32_t> incomplete_at;
57+
58+
Role role;
59+
60+
// The content of the message in array of text and/or images.
61+
std::vector<std::unique_ptr<Content>> content;
62+
63+
// If applicable, the ID of the assistant that authored this message.
64+
std::optional<std::string> assistant_id;
65+
66+
// The ID of the run associated with the creation of this message. Value is null when messages are created manually using the create message or create thread endpoints.
67+
std::optional<std::string> run_id;
68+
69+
// A list of files attached to the message, and the tools they were added to.
70+
std::optional<std::vector<Attachment>> attachments;
71+
72+
// Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional information about the object in a structured format. Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long.
73+
Cortex::VariantMap metadata;
74+
75+
static cpp::result<Message, std::string> FromJsonString(
76+
std::string&& json_str) {
77+
Json::Value root;
78+
Json::Reader reader;
79+
if (!reader.parse(json_str, root)) {
80+
return cpp::fail("Failed to parse JSON: " +
81+
reader.getFormattedErrorMessages());
82+
}
83+
84+
Message message;
85+
86+
try {
87+
message.id = std::move(root["id"].asString());
88+
message.object =
89+
std::move(root.get("object", "thread.message").asString());
90+
message.created_at = root["created_at"].asUInt();
91+
if (message.created_at == 0 && root["created"].asUInt64() != 0) {
92+
message.created_at = root["created"].asUInt64() / 1000;
93+
}
94+
message.thread_id = std::move(root["thread_id"].asString());
95+
message.status = StatusFromString(std::move(root["status"].asString()));
96+
97+
message.incomplete_details =
98+
IncompleteDetail::FromJson(std::move(root["incomplete_details"]))
99+
.value();
100+
message.completed_at = root["completed_at"].asUInt();
101+
message.incomplete_at = root["incomplete_at"].asUInt();
102+
message.role = RoleFromString(std::move(root["role"].asString()));
103+
message.content = ParseContents(std::move(root["content"])).value();
104+
105+
message.assistant_id = std::move(root["assistant_id"].asString());
106+
message.run_id = std::move(root["run_id"].asString());
107+
message.attachments =
108+
ParseAttachments(std::move(root["attachments"])).value();
109+
110+
if (root["metadata"].isObject() && !root["metadata"].empty()) {
111+
auto res = Cortex::ConvertJsonValueToMap(root["metadata"]);
112+
if (res.has_error()) {
113+
CTL_WRN("Failed to convert metadata to map: " + res.error());
114+
} else {
115+
message.metadata = res.value();
116+
}
117+
}
118+
119+
return message;
120+
} catch (const std::exception& e) {
121+
return cpp::fail(std::string("FromJsonString failed: ") + e.what());
122+
}
123+
}
124+
125+
cpp::result<std::string, std::string> ToSingleLineJsonString() {
126+
auto json_result = ToJson();
127+
if (json_result.has_error()) {
128+
return cpp::fail(json_result.error());
129+
}
130+
131+
Json::FastWriter writer;
132+
try {
133+
return writer.write(json_result.value());
134+
} catch (const std::exception& e) {
135+
return cpp::fail(std::string("Failed to write JSON: ") + e.what());
136+
}
137+
}
138+
139+
cpp::result<Json::Value, std::string> ToJson() override {
140+
try {
141+
Json::Value json;
142+
143+
json["id"] = id;
144+
json["object"] = object;
145+
json["created_at"] = created_at;
146+
json["thread_id"] = thread_id;
147+
json["status"] = StatusToString(status);
148+
149+
if (incomplete_details.has_value()) {
150+
if (auto it = incomplete_details->ToJson(); it.has_value()) {
151+
json["incomplete_details"] = it.value();
152+
} else {
153+
CTL_WRN("Failed to convert incomplete_details to json: " +
154+
it.error());
155+
}
156+
}
157+
if (completed_at.has_value() && completed_at.value() != 0) {
158+
json["completed_at"] = *completed_at;
159+
}
160+
if (incomplete_at.has_value() && incomplete_at.value() != 0) {
161+
json["incomplete_at"] = *incomplete_at;
162+
}
163+
164+
json["role"] = RoleToString(role);
165+
166+
Json::Value content_json_arr{Json::arrayValue};
167+
for (auto& child_content : content) {
168+
if (auto it = child_content->ToJson(); it.has_value()) {
169+
content_json_arr.append(it.value());
170+
} else {
171+
CTL_WRN("Failed to convert content to json: " + it.error());
172+
}
173+
}
174+
json["content"] = content_json_arr;
175+
if (assistant_id.has_value() && !assistant_id->empty()) {
176+
json["assistant_id"] = *assistant_id;
177+
}
178+
if (run_id.has_value() && !run_id->empty()) {
179+
json["run_id"] = *run_id;
180+
}
181+
if (attachments.has_value()) {
182+
Json::Value attachments_json_arr{Json::arrayValue};
183+
for (auto& attachment : *attachments) {
184+
if (auto it = attachment.ToJson(); it.has_value()) {
185+
attachments_json_arr.append(it.value());
186+
} else {
187+
CTL_WRN("Failed to convert attachment to json: " + it.error());
188+
}
189+
}
190+
json["attachments"] = attachments_json_arr;
191+
}
192+
193+
Json::Value metadata_json{Json::objectValue};
194+
for (const auto& [key, value] : metadata) {
195+
if (std::holds_alternative<bool>(value)) {
196+
metadata_json[key] = std::get<bool>(value);
197+
} else if (std::holds_alternative<uint64_t>(value)) {
198+
metadata_json[key] = std::get<uint64_t>(value);
199+
} else if (std::holds_alternative<double>(value)) {
200+
metadata_json[key] = std::get<double>(value);
201+
} else {
202+
metadata_json[key] = std::get<std::string>(value);
203+
}
204+
}
205+
json["metadata"] = metadata_json;
206+
207+
return json;
208+
} catch (const std::exception& e) {
209+
return cpp::fail(std::string("ToJson failed: ") + e.what());
210+
}
211+
}
212+
};
213+
}; // namespace ThreadMessage

engine/common/message_attachment.h

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#pragma once
2+
3+
#include <json/reader.h>
4+
#include "common/json_serializable.h"
5+
6+
namespace ThreadMessage {
7+
8+
// The tools to add this file to.
9+
struct Tool {
10+
std::string type;
11+
12+
Tool(const std::string& type) : type{type} {}
13+
};
14+
15+
// The type of tool being defined: code_interpreter
16+
struct CodeInterpreter : Tool {
17+
CodeInterpreter() : Tool{"code_interpreter"} {}
18+
};
19+
20+
// The type of tool being defined: file_search
21+
struct FileSearch : Tool {
22+
FileSearch() : Tool{"file_search"} {}
23+
};
24+
25+
// A list of files attached to the message, and the tools they were added to.
26+
struct Attachment : JsonSerializable {
27+
28+
// The ID of the file to attach to the message.
29+
std::string file_id;
30+
31+
std::vector<Tool> tools;
32+
33+
cpp::result<Json::Value, std::string> ToJson() override {
34+
try {
35+
Json::Value json;
36+
json["file_id"] = file_id;
37+
Json::Value tools_json_arr{Json::arrayValue};
38+
for (auto& tool : tools) {
39+
Json::Value tool_json;
40+
tool_json["type"] = tool.type;
41+
tools_json_arr.append(tool_json);
42+
}
43+
json["tools"] = tools_json_arr;
44+
return json;
45+
} catch (const std::exception& e) {
46+
return cpp::fail(std::string("ToJson failed: ") + e.what());
47+
}
48+
}
49+
};
50+
}; // namespace ThreadMessage
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#include <optional>
2+
#include "common/message_attachment.h"
3+
#include "utils/result.hpp"
4+
5+
namespace ThreadMessage {
6+
inline cpp::result<Attachment, std::string> ParseAttachment(
7+
Json::Value&& json) {
8+
if (json.empty()) {
9+
return cpp::fail("Json string is empty");
10+
}
11+
12+
Attachment attachment;
13+
attachment.file_id = json["file_id"].asString();
14+
15+
std::vector<Tool> tools{};
16+
if (json["tools"].isArray()) {
17+
for (auto& tool_json : json["tools"]) {
18+
Tool tool{tool_json["type"].asString()};
19+
tools.push_back(tool);
20+
}
21+
}
22+
attachment.tools = tools;
23+
24+
return attachment;
25+
}
26+
27+
inline cpp::result<std::optional<std::vector<Attachment>>, std::string>
28+
ParseAttachments(Json::Value&& json) {
29+
if (json.empty()) {
30+
// still count as success
31+
return std::nullopt;
32+
}
33+
if (!json.isArray()) {
34+
return cpp::fail("Json is not an array");
35+
}
36+
37+
std::vector<Attachment> attachments;
38+
for (auto& attachment_json : json) {
39+
auto attachment = ParseAttachment(std::move(attachment_json));
40+
if (attachment.has_error()) {
41+
return cpp::fail(attachment.error());
42+
}
43+
attachments.push_back(attachment.value());
44+
}
45+
46+
return attachments;
47+
}
48+
}; // namespace ThreadMessage

engine/common/message_content.h

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#pragma once
2+
3+
#include <string>
4+
#include "common/json_serializable.h"
5+
6+
namespace ThreadMessage {
7+
8+
struct Content : JsonSerializable {
9+
std::string type;
10+
11+
Content(const std::string& type) : type{type} {}
12+
13+
Content(const Content&) = delete;
14+
15+
Content& operator=(const Content&) = delete;
16+
17+
Content(Content&&) noexcept = default;
18+
19+
Content& operator=(Content&&) noexcept = default;
20+
21+
virtual ~Content() = default;
22+
};
23+
}; // namespace ThreadMessage

0 commit comments

Comments
 (0)