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

Commit 05dc472

Browse files
committed
feat: add files api
1 parent 9694ec8 commit 05dc472

File tree

9 files changed

+660
-7
lines changed

9 files changed

+660
-7
lines changed

engine/common/file.h

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#pragma once
2+
3+
#include <string>
4+
#include "common/json_serializable.h"
5+
6+
namespace OpenAi {
7+
/**
8+
* The File object represents a document that has been uploaded to OpenAI.
9+
*/
10+
struct File : public JsonSerializable {
11+
/**
12+
* The file identifier, which can be referenced in the API endpoints.
13+
*/
14+
std::string id;
15+
16+
/**
17+
* The object type, which is always file.
18+
*/
19+
std::string object = "file";
20+
21+
/**
22+
* The size of the file, in bytes.
23+
*/
24+
uint64_t bytes;
25+
26+
/**
27+
* The Unix timestamp (in seconds) for when the file was created.
28+
*/
29+
uint32_t created_at;
30+
31+
/**
32+
* The name of the file.
33+
*/
34+
std::string filename;
35+
36+
/**
37+
* The intended purpose of the file. Supported values are assistants,
38+
* assistants_output, batch, batch_output, fine-tune, fine-tune-results
39+
* and vision.
40+
*/
41+
std::string purpose;
42+
43+
~File() = default;
44+
45+
static cpp::result<File, std::string> FromJson(const Json::Value& json) {
46+
File file;
47+
48+
file.id = std::move(json["id"].asString());
49+
file.object = "file";
50+
file.bytes = json["bytes"].asUInt64();
51+
file.created_at = json["created_at"].asUInt();
52+
file.filename = std::move(json["filename"].asString());
53+
file.purpose = std::move(json["purpose"].asString());
54+
55+
return file;
56+
}
57+
58+
cpp::result<Json::Value, std::string> ToJson() {
59+
Json::Value root;
60+
61+
root["id"] = id;
62+
root["object"] = object;
63+
root["bytes"] = bytes;
64+
root["created_at"] = created_at;
65+
root["filename"] = filename;
66+
root["purpose"] = purpose;
67+
68+
return root;
69+
}
70+
};
71+
} // namespace OpenAi
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#pragma once
2+
3+
#include "common/file.h"
4+
#include "utils/result.hpp"
5+
6+
class FileRepository {
7+
public:
8+
virtual cpp::result<void, std::string> StoreFile(OpenAi::File& file_metadata,
9+
const char* content,
10+
uint64_t length) = 0;
11+
12+
virtual cpp::result<std::vector<OpenAi::File>, std::string> ListFiles(
13+
const std::string& purpose, uint8_t limit, const std::string& order,
14+
const std::string& after) const = 0;
15+
16+
virtual cpp::result<OpenAi::File, std::string> RetrieveFile(
17+
const std::string file_id) const = 0;
18+
19+
virtual cpp::result<std::pair<std::unique_ptr<char[]>, size_t>, std::string>
20+
RetrieveFileContent(const std::string& file_id) const = 0;
21+
22+
virtual cpp::result<void, std::string> DeleteFile(
23+
const std::string& file_id) = 0;
24+
25+
virtual ~FileRepository() = default;
26+
};

engine/controllers/files.cc

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
#include "files.h"
2+
#include "common/api-dto/delete_success_response.h"
3+
#include "utils/cortex_utils.h"
4+
#include "utils/logging_utils.h"
5+
6+
void Files::UploadFile(const HttpRequestPtr& req,
7+
std::function<void(const HttpResponsePtr&)>&& callback) {
8+
MultiPartParser parser;
9+
if (parser.parse(req) != 0 || parser.getFiles().size() != 1) {
10+
Json::Value root;
11+
root["message"] = "Must only be one file";
12+
auto response = cortex_utils::CreateCortexHttpJsonResponse(root);
13+
response->setStatusCode(k400BadRequest);
14+
callback(response);
15+
return;
16+
}
17+
18+
auto params = parser.getParameters();
19+
if (params.find("purpose") == params.end()) {
20+
Json::Value root;
21+
root["message"] = "purpose is mandatory";
22+
auto response = cortex_utils::CreateCortexHttpJsonResponse(root);
23+
response->setStatusCode(k400BadRequest);
24+
callback(response);
25+
return;
26+
}
27+
28+
auto purpose = params["purpose"];
29+
if (std::find(file_service_->kSupportedPurposes.begin(),
30+
file_service_->kSupportedPurposes.end(),
31+
purpose) == file_service_->kSupportedPurposes.end()) {
32+
Json::Value root;
33+
root["message"] =
34+
"purpose is not supported. Purpose can only one of these types: "
35+
"assistants, vision, batch or fine-tune";
36+
auto response = cortex_utils::CreateCortexHttpJsonResponse(root);
37+
response->setStatusCode(k400BadRequest);
38+
callback(response);
39+
return;
40+
}
41+
42+
const auto& file = parser.getFiles()[0];
43+
auto result =
44+
file_service_->UploadFile(file.getFileName(), purpose,
45+
file.fileContent().data(), file.fileLength());
46+
47+
if (result.has_error()) {
48+
Json::Value ret;
49+
ret["message"] = result.error();
50+
auto resp = cortex_utils::CreateCortexHttpJsonResponse(ret);
51+
resp->setStatusCode(k400BadRequest);
52+
callback(resp);
53+
} else {
54+
auto resp =
55+
cortex_utils::CreateCortexHttpJsonResponse(result->ToJson().value());
56+
resp->setStatusCode(k200OK);
57+
callback(resp);
58+
}
59+
}
60+
61+
void Files::ListFiles(const HttpRequestPtr& req,
62+
std::function<void(const HttpResponsePtr&)>&& callback,
63+
std::optional<std::string> purpose,
64+
std::optional<std::string> limit,
65+
std::optional<std::string> order,
66+
std::optional<std::string> after) const {
67+
auto res = file_service_->ListFiles(
68+
purpose.value_or(""), std::stoi(limit.value_or("20")),
69+
order.value_or("desc"), after.value_or(""));
70+
if (res.has_error()) {
71+
Json::Value root;
72+
root["message"] = res.error();
73+
auto response = cortex_utils::CreateCortexHttpJsonResponse(root);
74+
response->setStatusCode(k400BadRequest);
75+
callback(response);
76+
return;
77+
}
78+
79+
Json::Value msg_arr(Json::arrayValue);
80+
for (auto& msg : res.value()) {
81+
if (auto it = msg.ToJson(); it.has_value()) {
82+
msg_arr.append(it.value());
83+
} else {
84+
CTL_WRN("Failed to convert message to json: " + it.error());
85+
}
86+
}
87+
88+
Json::Value root;
89+
root["object"] = "list";
90+
root["data"] = msg_arr;
91+
auto response = cortex_utils::CreateCortexHttpJsonResponse(root);
92+
response->setStatusCode(k200OK);
93+
callback(response);
94+
}
95+
96+
void Files::RetrieveFile(const HttpRequestPtr& req,
97+
std::function<void(const HttpResponsePtr&)>&& callback,
98+
const std::string& file_id) const {
99+
auto res = file_service_->RetrieveFile(file_id);
100+
if (res.has_error()) {
101+
Json::Value ret;
102+
ret["message"] = res.error();
103+
auto resp = cortex_utils::CreateCortexHttpJsonResponse(ret);
104+
resp->setStatusCode(k400BadRequest);
105+
callback(resp);
106+
return;
107+
}
108+
109+
auto resp = cortex_utils::CreateCortexHttpJsonResponse(res->ToJson().value());
110+
resp->setStatusCode(k200OK);
111+
callback(resp);
112+
}
113+
114+
void Files::DeleteFile(const HttpRequestPtr& req,
115+
std::function<void(const HttpResponsePtr&)>&& callback,
116+
const std::string& file_id) {
117+
auto res = file_service_->DeleteFile(file_id);
118+
if (res.has_error()) {
119+
Json::Value ret;
120+
ret["message"] = res.error();
121+
auto resp = cortex_utils::CreateCortexHttpJsonResponse(ret);
122+
resp->setStatusCode(k400BadRequest);
123+
callback(resp);
124+
return;
125+
}
126+
127+
api_response::DeleteSuccessResponse response;
128+
response.id = file_id;
129+
response.object = "file";
130+
response.deleted = true;
131+
auto resp =
132+
cortex_utils::CreateCortexHttpJsonResponse(response.ToJson().value());
133+
resp->setStatusCode(k200OK);
134+
callback(resp);
135+
}
136+
137+
void Files::RetrieveFileContent(
138+
const HttpRequestPtr& req,
139+
std::function<void(const HttpResponsePtr&)>&& callback,
140+
const std::string& file_id) {
141+
auto res = file_service_->RetrieveFileContent(file_id);
142+
if (res.has_error()) {
143+
Json::Value ret;
144+
ret["message"] = res.error();
145+
auto resp = cortex_utils::CreateCortexHttpJsonResponse(ret);
146+
resp->setStatusCode(k400BadRequest);
147+
callback(resp);
148+
return;
149+
}
150+
151+
auto [buffer, size] = std::move(res.value());
152+
auto resp = HttpResponse::newHttpResponse();
153+
resp->setBody(std::string(buffer.get(), size));
154+
resp->setContentTypeCode(CT_APPLICATION_OCTET_STREAM);
155+
callback(resp);
156+
}

engine/controllers/files.h

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
#pragma once
2+
3+
#include <drogon/HttpController.h>
4+
#include <trantor/utils/Logger.h>
5+
#include <optional>
6+
#include "services/file_service.h"
7+
8+
using namespace drogon;
9+
10+
class Files : public drogon::HttpController<Files, false> {
11+
public:
12+
METHOD_LIST_BEGIN
13+
ADD_METHOD_TO(Files::UploadFile, "/v1/files", Options, Post);
14+
15+
ADD_METHOD_TO(Files::RetrieveFile, "/v1/files/{file_id}", Get);
16+
17+
ADD_METHOD_TO(
18+
Files::ListFiles,
19+
"/v1/files?purpose={purpose}&limit={limit}&order={order}&after={after}",
20+
Get);
21+
22+
ADD_METHOD_TO(Files::DeleteFile, "/v1/files/{file_id}", Options, Delete);
23+
24+
ADD_METHOD_TO(Files::RetrieveFileContent, "/v1/files/{file_id}/content", Get);
25+
26+
METHOD_LIST_END
27+
28+
explicit Files(std::shared_ptr<FileService> file_service)
29+
: file_service_{file_service} {}
30+
31+
void UploadFile(const HttpRequestPtr& req,
32+
std::function<void(const HttpResponsePtr&)>&& callback);
33+
34+
void ListFiles(const HttpRequestPtr& req,
35+
std::function<void(const HttpResponsePtr&)>&& callback,
36+
std::optional<std::string> purpose,
37+
std::optional<std::string> limit,
38+
std::optional<std::string> order,
39+
std::optional<std::string> after) const;
40+
41+
void RetrieveFile(const HttpRequestPtr& req,
42+
std::function<void(const HttpResponsePtr&)>&& callback,
43+
const std::string& file_id) const;
44+
45+
void DeleteFile(const HttpRequestPtr& req,
46+
std::function<void(const HttpResponsePtr&)>&& callback,
47+
const std::string& file_id);
48+
49+
void RetrieveFileContent(
50+
const HttpRequestPtr& req,
51+
std::function<void(const HttpResponsePtr&)>&& callback,
52+
const std::string& file_id);
53+
54+
private:
55+
std::shared_ptr<FileService> file_service_;
56+
};

engine/main.cc

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include "controllers/configs.h"
66
#include "controllers/engines.h"
77
#include "controllers/events.h"
8+
#include "controllers/files.h"
89
#include "controllers/hardware.h"
910
#include "controllers/messages.h"
1011
#include "controllers/models.h"
@@ -13,6 +14,7 @@
1314
#include "controllers/threads.h"
1415
#include "database/database.h"
1516
#include "migrations/migration_manager.h"
17+
#include "repositories/file_fs_repository.h"
1618
#include "repositories/message_fs_repository.h"
1719
#include "repositories/thread_fs_repository.h"
1820
#include "services/assistant_service.h"
@@ -121,11 +123,13 @@ void RunServer(std::optional<int> port, bool ignore_cout) {
121123
auto event_queue_ptr = std::make_shared<EventQueue>();
122124
cortex::event::EventProcessor event_processor(event_queue_ptr);
123125

124-
auto msg_repo = std::make_shared<MessageFsRepository>(
125-
file_manager_utils::GetCortexDataPath());
126-
auto thread_repo = std::make_shared<ThreadFsRepository>(
127-
file_manager_utils::GetCortexDataPath());
126+
auto data_folder_path = file_manager_utils::GetCortexDataPath();
128127

128+
auto file_repo = std::make_shared<FileFsRepository>(data_folder_path);
129+
auto msg_repo = std::make_shared<MessageFsRepository>(data_folder_path);
130+
auto thread_repo = std::make_shared<ThreadFsRepository>(data_folder_path);
131+
132+
auto file_srv = std::make_shared<FileService>(file_repo);
129133
auto assistant_srv = std::make_shared<AssistantService>(thread_repo);
130134
auto thread_srv = std::make_shared<ThreadService>(thread_repo);
131135
auto message_srv = std::make_shared<MessageService>(msg_repo);
@@ -145,6 +149,7 @@ void RunServer(std::optional<int> port, bool ignore_cout) {
145149
file_watcher_srv->start();
146150

147151
// initialize custom controllers
152+
auto file_ctl = std::make_shared<Files>(file_srv);
148153
auto assistant_ctl = std::make_shared<Assistants>(assistant_srv);
149154
auto thread_ctl = std::make_shared<Threads>(thread_srv, message_srv);
150155
auto message_ctl = std::make_shared<Messages>(message_srv);
@@ -157,6 +162,7 @@ void RunServer(std::optional<int> port, bool ignore_cout) {
157162
std::make_shared<inferences::server>(inference_svc, engine_service);
158163
auto config_ctl = std::make_shared<Configs>(config_service);
159164

165+
drogon::app().registerController(file_ctl);
160166
drogon::app().registerController(assistant_ctl);
161167
drogon::app().registerController(thread_ctl);
162168
drogon::app().registerController(message_ctl);
@@ -168,9 +174,6 @@ void RunServer(std::optional<int> port, bool ignore_cout) {
168174
drogon::app().registerController(hw_ctl);
169175
drogon::app().registerController(config_ctl);
170176

171-
auto upload_path = std::filesystem::temp_directory_path() / "cortex-uploads";
172-
drogon::app().setUploadPath(upload_path.string());
173-
174177
LOG_INFO << "Server started, listening at: " << config.apiServerHost << ":"
175178
<< config.apiServerPort;
176179
LOG_INFO << "Please load your model";
@@ -185,6 +188,12 @@ void RunServer(std::optional<int> port, bool ignore_cout) {
185188
LOG_INFO << "Number of thread is:" << drogon::app().getThreadNum();
186189
drogon::app().disableSigtermHandling();
187190

191+
// file upload
192+
drogon::app()
193+
.enableCompressedRequest(true)
194+
.setClientMaxBodySize(256 * 1024 * 1024) // Max 256MiB body size
195+
.setClientMaxMemoryBodySize(1024 * 1024); // 1MiB before writing to disk
196+
188197
// CORS
189198
drogon::app().registerPostHandlingAdvice(
190199
[config_service](const drogon::HttpRequestPtr& req,

0 commit comments

Comments
 (0)