feat: auto LLM feedback runner + problem link + 5xx retry
- Add SubmissionFeedbackRunner: async background queue for auto LLM feedback
- Enqueue feedback generation after each submission in submitProblem()
- Register runner in main.cc with CSP_FEEDBACK_AUTO_RUN env var
- Add problem_title to GET /api/v1/submissions/{id} response
- Frontend: clickable problem link on submission detail page
- Enhance LLM prompt with richer analysis dimensions
- Add 5xx/connection error retry (max 5 attempts) in Python LLM script
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
这个提交包含在:
@@ -6,6 +6,7 @@
|
||||
#include "csp/services/contest_service.h"
|
||||
#include "csp/services/problem_service.h"
|
||||
#include "csp/services/solution_access_service.h"
|
||||
#include "csp/services/submission_feedback_runner.h"
|
||||
#include "csp/services/submission_feedback_service.h"
|
||||
#include "csp/services/submission_service.h"
|
||||
#include "http_auth.h"
|
||||
@@ -130,6 +131,10 @@ void SubmissionController::submitProblem(
|
||||
|
||||
services::SubmissionService svc(csp::AppState::Instance().db());
|
||||
auto s = svc.CreateAndJudge(create);
|
||||
|
||||
// Auto-enqueue LLM feedback generation in background.
|
||||
services::SubmissionFeedbackRunner::Instance().Enqueue(s.id);
|
||||
|
||||
cb(JsonOk(domain::ToJson(s)));
|
||||
} catch (const std::invalid_argument&) {
|
||||
cb(JsonError(drogon::k400BadRequest, "invalid numeric field"));
|
||||
@@ -187,6 +192,14 @@ void SubmissionController::getSubmission(
|
||||
Json::Value payload = domain::ToJson(*s);
|
||||
payload["code"] = s->code;
|
||||
|
||||
// Attach problem title for frontend linking.
|
||||
{
|
||||
services::ProblemService psvc(csp::AppState::Instance().db());
|
||||
if (const auto p = psvc.GetById(s->problem_id); p.has_value()) {
|
||||
payload["problem_title"] = p->title;
|
||||
}
|
||||
}
|
||||
|
||||
services::SolutionAccessService access_svc(csp::AppState::Instance().db());
|
||||
const auto stats =
|
||||
access_svc.QueryUserProblemViewStats(s->user_id, s->problem_id);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "csp/services/kb_import_runner.h"
|
||||
#include "csp/services/problem_gen_runner.h"
|
||||
#include "csp/services/problem_solution_runner.h"
|
||||
#include "csp/services/submission_feedback_runner.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
@@ -20,6 +21,7 @@ int main(int argc, char** argv) {
|
||||
csp::services::KbImportRunner::Instance().Configure(db_path);
|
||||
csp::services::ProblemSolutionRunner::Instance().Configure(db_path);
|
||||
csp::services::ProblemGenRunner::Instance().Configure(db_path);
|
||||
csp::services::SubmissionFeedbackRunner::Instance().Configure(db_path);
|
||||
|
||||
// Optional seed admin user for dev/test.
|
||||
{
|
||||
@@ -48,6 +50,9 @@ int main(int argc, char** argv) {
|
||||
csp::AppState::Instance().db());
|
||||
// Auto-generate one CSP-J new problem (RAG + dedupe) on startup by default.
|
||||
csp::services::ProblemGenRunner::Instance().AutoStartIfEnabled();
|
||||
// Auto-queue submission feedback generation for submissions without feedback.
|
||||
csp::services::SubmissionFeedbackRunner::Instance().AutoStartIfEnabled(
|
||||
csp::AppState::Instance().db());
|
||||
|
||||
// CORS (dev-friendly). In production, prefer reverse proxy same-origin.
|
||||
drogon::app().registerPreRoutingAdvice([](const drogon::HttpRequestPtr& req,
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
#include "csp/services/submission_feedback_runner.h"
|
||||
|
||||
#include "csp/services/problem_service.h"
|
||||
#include "csp/services/submission_feedback_service.h"
|
||||
#include "csp/services/submission_service.h"
|
||||
|
||||
#include <drogon/drogon.h>
|
||||
#include <sqlite3.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace csp::services {
|
||||
|
||||
namespace {
|
||||
|
||||
bool EnvBool(const char* key, bool default_value) {
|
||||
const char* raw = std::getenv(key);
|
||||
if (!raw) return default_value;
|
||||
std::string val(raw);
|
||||
for (auto& c : val)
|
||||
c = static_cast<char>(::tolower(static_cast<unsigned char>(c)));
|
||||
if (val == "1" || val == "true" || val == "yes" || val == "on") return true;
|
||||
if (val == "0" || val == "false" || val == "no" || val == "off") return false;
|
||||
return default_value;
|
||||
}
|
||||
|
||||
int EnvInt(const char* key, int default_value) {
|
||||
const char* raw = std::getenv(key);
|
||||
if (!raw) return default_value;
|
||||
try {
|
||||
return std::stoi(raw);
|
||||
} catch (...) {
|
||||
return default_value;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SubmissionFeedbackRunner& SubmissionFeedbackRunner::Instance() {
|
||||
static SubmissionFeedbackRunner inst;
|
||||
return inst;
|
||||
}
|
||||
|
||||
void SubmissionFeedbackRunner::Configure(std::string db_path) {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
db_path_ = std::move(db_path);
|
||||
RecoverPendingLocked();
|
||||
StartWorkerIfNeededLocked();
|
||||
}
|
||||
|
||||
bool SubmissionFeedbackRunner::Enqueue(int64_t submission_id) {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
if (db_path_.empty()) return false;
|
||||
queue_.push_back(submission_id);
|
||||
++pending_jobs_;
|
||||
StartWorkerIfNeededLocked();
|
||||
return true;
|
||||
}
|
||||
|
||||
void SubmissionFeedbackRunner::AutoStartIfEnabled(db::SqliteDb& db) {
|
||||
if (!EnvBool("CSP_FEEDBACK_AUTO_RUN", false)) {
|
||||
LOG_INFO << "submission feedback auto-run disabled";
|
||||
return;
|
||||
}
|
||||
|
||||
const int limit =
|
||||
std::max(1, std::min(10000, EnvInt("CSP_FEEDBACK_AUTO_LIMIT", 500)));
|
||||
|
||||
// Find submissions without feedback.
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
const char* sql =
|
||||
"SELECT s.id FROM submissions s "
|
||||
"LEFT JOIN submission_feedback f ON f.submission_id=s.id "
|
||||
"WHERE f.id IS NULL "
|
||||
"ORDER BY s.id DESC LIMIT ?";
|
||||
if (sqlite3_prepare_v2(db.raw(), sql, -1, &stmt, nullptr) != SQLITE_OK) {
|
||||
LOG_ERROR << "feedback runner: failed to query pending submissions";
|
||||
return;
|
||||
}
|
||||
sqlite3_bind_int(stmt, 1, limit);
|
||||
|
||||
int queued = 0;
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
const int64_t sid = sqlite3_column_int64(stmt, 0);
|
||||
Enqueue(sid);
|
||||
++queued;
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
LOG_INFO << "submission feedback auto-run queued=" << queued
|
||||
<< ", pending=" << PendingCount();
|
||||
}
|
||||
|
||||
size_t SubmissionFeedbackRunner::PendingCount() const {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
return pending_jobs_;
|
||||
}
|
||||
|
||||
void SubmissionFeedbackRunner::StartWorkerIfNeededLocked() {
|
||||
if (worker_running_ || queue_.empty()) return;
|
||||
worker_running_ = true;
|
||||
std::thread([this]() { WorkerLoop(); }).detach();
|
||||
}
|
||||
|
||||
void SubmissionFeedbackRunner::WorkerLoop() {
|
||||
while (true) {
|
||||
int64_t submission_id = 0;
|
||||
std::string db_path;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
if (queue_.empty()) {
|
||||
worker_running_ = false;
|
||||
return;
|
||||
}
|
||||
submission_id = queue_.front();
|
||||
queue_.pop_front();
|
||||
db_path = db_path_;
|
||||
}
|
||||
|
||||
try {
|
||||
db::SqliteDb db = db::SqliteDb::OpenFile(db_path);
|
||||
SubmissionFeedbackService feedback_svc(db);
|
||||
|
||||
// Skip if feedback already exists.
|
||||
if (feedback_svc.GetBySubmissionId(submission_id).has_value()) {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
if (pending_jobs_ > 0) --pending_jobs_;
|
||||
continue;
|
||||
}
|
||||
|
||||
SubmissionService sub_svc(db);
|
||||
const auto submission = sub_svc.GetById(submission_id);
|
||||
if (!submission.has_value()) {
|
||||
LOG_WARN << "feedback runner: submission " << submission_id
|
||||
<< " not found, skipping";
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
if (pending_jobs_ > 0) --pending_jobs_;
|
||||
continue;
|
||||
}
|
||||
|
||||
ProblemService prob_svc(db);
|
||||
const auto problem = prob_svc.GetById(submission->problem_id);
|
||||
if (!problem.has_value()) {
|
||||
LOG_WARN << "feedback runner: problem " << submission->problem_id
|
||||
<< " not found for submission " << submission_id;
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
if (pending_jobs_ > 0) --pending_jobs_;
|
||||
continue;
|
||||
}
|
||||
|
||||
feedback_svc.GenerateAndSave(*submission, *problem, false);
|
||||
LOG_INFO << "feedback runner: generated feedback for submission "
|
||||
<< submission_id;
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR << "feedback runner: submission " << submission_id
|
||||
<< " failed: " << e.what();
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
if (pending_jobs_ > 0) --pending_jobs_;
|
||||
}
|
||||
|
||||
// Small delay between jobs to avoid overwhelming LLM API / SQLite.
|
||||
std::this_thread::sleep_for(std::chrono::seconds(2));
|
||||
}
|
||||
}
|
||||
|
||||
void SubmissionFeedbackRunner::RecoverPendingLocked() {
|
||||
if (recovered_ || db_path_.empty()) return;
|
||||
recovered_ = true;
|
||||
// Recovery is handled by AutoStartIfEnabled; no separate DB state to recover.
|
||||
}
|
||||
|
||||
} // namespace csp::services
|
||||
在新工单中引用
屏蔽一个用户