1use chrono::{DateTime, Utc};
2use serde_json::Value;
3use sqlx::{PgPool, Postgres, Row, Transaction};
4
5use agentics_domain::models::challenge::{ChallengeBundleSpec, TargetAccelerator};
6use agentics_domain::models::evaluation::{
7 EvaluationDto, EvaluationJobPayload, EvaluationJobStatus, EvaluationStatus, MetricValue,
8 ScoringMode, SolutionArtifactMetadata, SolutionSubmissionStatus,
9};
10use agentics_domain::models::ids::{AgentId, EvaluationJobId, SolutionSubmissionId};
11use agentics_domain::models::names::{ChallengeName, TargetName};
12use agentics_domain::storage::StorageKey;
13use agentics_error::{Result, ServiceError};
14
15use super::evaluation_policy::{
16 ensure_challenge_supports_eval_type_tx, ensure_validation_uses_public_bundle,
17 lock_active_challenge_for_admission_tx,
18};
19use super::ids::{agent_id_from_row, challenge_name_from_row, solution_submission_id_from_row};
20use super::json::decode_optional_json;
21
22mod admission;
23mod rows;
24use admission::enforce_quota_admission;
25use rows::{
26 artifact_metadata_from_row, count_to_u64, optional_evaluation_job_id_from_row,
27 optional_evaluation_job_status_from_row, optional_evaluation_status_from_row,
28 optional_scoring_mode_from_row, optional_solution_submission_id_from_row, parse_eval_from_row,
29 solution_submission_status_from_row, storage_key_from_row, target_from_row, u64_to_i64,
30};
31
32#[derive(Debug, Clone)]
34pub struct CreateSolutionSubmissionInput {
35 pub solution_submission_id: SolutionSubmissionId,
36 pub job_id: EvaluationJobId,
37 pub agent_id: AgentId,
38 pub challenge_name: ChallengeName,
39 pub target: TargetName,
40 pub artifact_key: StorageKey,
41 pub artifact_metadata: SolutionArtifactMetadata,
42 pub note: String,
43 pub eval_type: ScoringMode,
44 pub explanation: String,
45 pub parent_solution_submission_id: Option<SolutionSubmissionId>,
46 pub credit_text: String,
47 pub quota_admission: SolutionSubmissionQuotaAdmission,
48}
49
50#[derive(Debug, Clone)]
52pub struct AdminSolutionSubmissionListItemRecord {
53 pub id: SolutionSubmissionId,
54 pub challenge_name: ChallengeName,
55 pub challenge_title: String,
56 pub target: TargetName,
57 pub agent_id: AgentId,
58 pub agent_display_name: String,
59 pub status: SolutionSubmissionStatus,
60 pub note: String,
61 pub visible_after_eval: bool,
62 pub latest_job_id: Option<EvaluationJobId>,
63 pub latest_job_status: Option<EvaluationJobStatus>,
64 pub latest_job_eval_type: Option<ScoringMode>,
65 pub validation_status: Option<EvaluationStatus>,
66 pub official_status: Option<EvaluationStatus>,
67 pub created_at: DateTime<Utc>,
68 pub updated_at: DateTime<Utc>,
69}
70
71#[derive(Debug, Clone)]
73pub struct PublicSolutionSubmissionListItemRecord {
74 pub id: SolutionSubmissionId,
75 pub challenge_name: ChallengeName,
76 pub target: TargetName,
77 pub challenge_title: String,
78 pub agent_id: AgentId,
79 pub agent_display_name: String,
80 pub status: SolutionSubmissionStatus,
81 pub note: String,
82 pub explanation: String,
83 pub parent_solution_submission_id: Option<SolutionSubmissionId>,
84 pub credit_text: String,
85 pub official_metrics: Vec<MetricValue>,
86 pub created_at: DateTime<Utc>,
87 pub updated_at: DateTime<Utc>,
88}
89
90#[derive(Debug, Clone, Copy)]
92pub struct PublicObserverStatsRecord {
93 pub challenge_count: u64,
94 pub agent_count: u64,
95 pub public_completed_submission_count: u64,
96 pub total_solution_attempt_count: u64,
97}
98
99#[derive(Debug, Clone, Copy)]
101pub struct SolutionSubmissionQuotaAdmission {
102 pub window_seconds: i64,
103 pub per_agent_challenge_limit: i64,
104 pub challenge_lifetime_limit: Option<i64>,
105 pub max_active_official_jobs: Option<i64>,
106}
107
108#[derive(Debug, Clone)]
110pub struct SolutionSubmissionRecord {
111 pub id: SolutionSubmissionId,
112 pub challenge_name: ChallengeName,
113 pub target: TargetName,
114 pub agent_id: AgentId,
115 pub agent_display_name: Option<String>,
116 pub challenge_title: Option<String>,
117 pub challenge_spec: ChallengeBundleSpec,
118 pub artifact_key: StorageKey,
119 pub artifact_metadata: Option<SolutionArtifactMetadata>,
120 pub note: String,
121 pub status: String,
122 pub explanation: String,
123 pub parent_solution_submission_id: Option<SolutionSubmissionId>,
124 pub credit_text: String,
125 pub visible_after_eval: bool,
126 pub created_at: DateTime<Utc>,
127 pub updated_at: DateTime<Utc>,
128 pub evaluation_job_id: Option<EvaluationJobId>,
129 pub evaluation_job_status: Option<String>,
130 pub evaluation: Option<EvaluationDto>,
131 pub validation_evaluation: Option<EvaluationDto>,
132 pub official_evaluation: Option<EvaluationDto>,
133}
134
135pub async fn create_solution_submission_with_job(
137 pool: &PgPool,
138 input: &CreateSolutionSubmissionInput,
139) -> Result<SolutionSubmissionRecord> {
140 let mut tx = pool.begin().await?;
141 let challenge = lock_active_challenge_for_admission_tx(&mut tx, &input.challenge_name).await?;
142 let spec: ChallengeBundleSpec = serde_json::from_value(challenge.spec_json.clone())
143 .map_err(|e| ServiceError::Internal(e.to_string()))?;
144 ensure_challenge_supports_eval_type_tx(
145 &mut tx,
146 &challenge.challenge_name,
147 &spec,
148 &input.target,
149 input.eval_type,
150 &input.agent_id,
151 )
152 .await?;
153 ensure_validation_uses_public_bundle(
154 input.eval_type,
155 &spec,
156 &challenge.bundle_key,
157 &challenge.public_bundle_key,
158 )?;
159 enforce_quota_admission(&mut tx, input).await?;
160 ensure_parent_solution_submission_matches_scope_tx(
161 &mut tx,
162 input.parent_solution_submission_id.as_ref(),
163 &input.agent_id,
164 &challenge.challenge_name,
165 &input.target,
166 )
167 .await?;
168
169 let row = sqlx::query(
170 r#"
171 INSERT INTO solution_submissions (
172 id, challenge_name, target, agent_id, artifact_key, note,
173 artifact_zip_bytes, artifact_uncompressed_bytes, artifact_file_count, artifact_sha256,
174 status, explanation, parent_solution_submission_id, credit_text, visible_after_eval
175 )
176 VALUES ($1::uuid, $2, $3, $4::uuid, $5, $6, $7, $8, $9, $10, 'pending', $11, $12::uuid, $13, FALSE)
177 RETURNING
178 id, challenge_name, target, agent_id, artifact_key, note,
179 artifact_zip_bytes, artifact_uncompressed_bytes, artifact_file_count, artifact_sha256,
180 status, explanation, parent_solution_submission_id, credit_text, visible_after_eval,
181 created_at, updated_at
182 "#,
183 )
184 .bind(input.solution_submission_id.as_str())
185 .bind(challenge.challenge_name.as_str())
186 .bind(input.target.as_str())
187 .bind(input.agent_id.as_str())
188 .bind(input.artifact_key.as_str())
189 .bind(&input.note)
190 .bind(u64_to_i64(
191 input.artifact_metadata.artifact_zip_bytes,
192 "artifact_zip_bytes",
193 )?)
194 .bind(u64_to_i64(
195 input.artifact_metadata.artifact_uncompressed_bytes,
196 "artifact_uncompressed_bytes",
197 )?)
198 .bind(u64_to_i64(
199 input.artifact_metadata.artifact_file_count,
200 "artifact_file_count",
201 )?)
202 .bind(input.artifact_metadata.artifact_sha256.to_string())
203 .bind(&input.explanation)
204 .bind(
205 input
206 .parent_solution_submission_id
207 .as_ref()
208 .map(SolutionSubmissionId::as_str),
209 )
210 .bind(&input.credit_text)
211 .fetch_one(&mut *tx)
212 .await?;
213
214 let payload = serde_json::to_value(EvaluationJobPayload {
215 artifact_key: input.artifact_key.clone(),
216 bundle_key: challenge.bundle_key.clone(),
217 public_bundle_key: challenge.public_bundle_key.clone(),
218 challenge_name: challenge.challenge_name.clone(),
219 target: input.target.clone(),
220 })
221 .map_err(|e| ServiceError::Internal(e.to_string()))?;
222
223 let priority = if input.eval_type == ScoringMode::Official {
224 10
225 } else {
226 0
227 };
228 let required_accelerator = required_accelerator_for_target(&spec, &input.target)?;
229
230 sqlx::query(
231 r#"
232 INSERT INTO evaluation_jobs (
233 id, solution_submission_id, challenge_name, target, required_accelerator, eval_type, status, priority, payload_json, scheduled_at
234 )
235 VALUES (
236 $1::uuid, $2::uuid, $3, $4, $5, $6, 'staged', $7, $8,
237 NOW()
238 )
239 "#,
240 )
241 .bind(input.job_id.as_str())
242 .bind(input.solution_submission_id.as_str())
243 .bind(challenge.challenge_name.as_str())
244 .bind(input.target.as_str())
245 .bind(required_accelerator.as_str())
246 .bind(input.eval_type.as_str())
247 .bind(priority)
248 .bind(&payload)
249 .execute(&mut *tx)
250 .await?;
251
252 tx.commit().await?;
253
254 Ok(SolutionSubmissionRecord {
255 id: solution_submission_id_from_row(&row, "id")?,
256 challenge_name: challenge.challenge_name,
257 target: target_from_row(&row, "target")?,
258 agent_id: agent_id_from_row(&row, "agent_id")?,
259 agent_display_name: None,
260 challenge_title: None,
261 challenge_spec: spec,
262 artifact_key: storage_key_from_row(&row, "artifact_key")?,
263 artifact_metadata: artifact_metadata_from_row(&row)?,
264 note: row.try_get("note")?,
265 status: row.try_get("status")?,
266 explanation: row.try_get("explanation")?,
267 parent_solution_submission_id: optional_solution_submission_id_from_row(
268 &row,
269 "parent_solution_submission_id",
270 )?,
271 credit_text: row.try_get("credit_text")?,
272 visible_after_eval: row.try_get("visible_after_eval")?,
273 created_at: row.try_get("created_at")?,
274 updated_at: row.try_get("updated_at")?,
275 evaluation_job_id: Some(input.job_id.clone()),
276 evaluation_job_status: Some("staged".to_string()),
277 evaluation: None,
278 validation_evaluation: None,
279 official_evaluation: None,
280 })
281}
282
283pub async fn ensure_parent_solution_submission_matches_scope(
285 pool: &PgPool,
286 parent_solution_submission_id: Option<&SolutionSubmissionId>,
287 agent_id: &AgentId,
288 challenge_name: &ChallengeName,
289 target: &TargetName,
290) -> Result<()> {
291 let mut tx = pool.begin().await?;
292 ensure_parent_solution_submission_matches_scope_tx(
293 &mut tx,
294 parent_solution_submission_id,
295 agent_id,
296 challenge_name,
297 target,
298 )
299 .await?;
300 tx.commit().await?;
301 Ok(())
302}
303
304fn required_accelerator_for_target(
306 spec: &ChallengeBundleSpec,
307 target: &TargetName,
308) -> Result<TargetAccelerator> {
309 let target_spec = spec.target(target).ok_or_else(|| {
310 ServiceError::Internal(format!(
311 "challenge `{}` does not declare target `{target}` after admission validation",
312 spec.challenge_name
313 ))
314 })?;
315 Ok(target_spec.accelerator)
316}
317
318async fn ensure_parent_solution_submission_matches_scope_tx<'a>(
320 tx: &mut Transaction<'a, Postgres>,
321 parent_solution_submission_id: Option<&SolutionSubmissionId>,
322 agent_id: &AgentId,
323 challenge_name: &ChallengeName,
324 target: &TargetName,
325) -> Result<()> {
326 let Some(parent_solution_submission_id) = parent_solution_submission_id else {
327 return Ok(());
328 };
329
330 let row = sqlx::query(
331 r#"
332 SELECT agent_id, challenge_name, target, status, visible_after_eval
333 FROM solution_submissions
334 WHERE id = $1::uuid
335 LIMIT 1
336 "#,
337 )
338 .bind(parent_solution_submission_id.as_str())
339 .fetch_optional(&mut **tx)
340 .await?;
341 let Some(row) = row else {
342 return Err(ServiceError::BadRequest(
343 "parent_solution_submission_id does not reference an existing solution submission"
344 .to_string(),
345 ));
346 };
347
348 let parent_agent_id = agent_id_from_row(&row, "agent_id")?;
349 let parent_challenge_name = challenge_name_from_row(&row, "challenge_name")?;
350 let parent_target = target_from_row(&row, "target")?;
351 let parent_status: String = row.try_get("status")?;
352 let parent_visible: bool = row.try_get("visible_after_eval")?;
353
354 if &parent_agent_id != agent_id
355 || &parent_challenge_name != challenge_name
356 || &parent_target != target
357 {
358 return Err(ServiceError::BadRequest(
359 "parent_solution_submission_id must belong to the same agent, challenge_name, and target"
360 .to_string(),
361 ));
362 }
363 if parent_status != SolutionSubmissionStatus::Completed.as_str() || !parent_visible {
364 return Err(ServiceError::BadRequest(
365 "parent_solution_submission_id must reference a completed visible solution submission"
366 .to_string(),
367 ));
368 }
369
370 Ok(())
371}
372
373pub async fn delete_solution_submission(
375 pool: &PgPool,
376 solution_submission_id: &SolutionSubmissionId,
377) -> Result<()> {
378 sqlx::query("DELETE FROM solution_submissions WHERE id = $1::uuid")
379 .bind(solution_submission_id.as_str())
380 .execute(pool)
381 .await?;
382 Ok(())
383}
384
385pub async fn get_solution_submission_by_id(
387 pool: &PgPool,
388 solution_submission_id: &SolutionSubmissionId,
389) -> Result<Option<SolutionSubmissionRecord>> {
390 get_solution_submission_by_id_inner(pool, solution_submission_id, false).await
391}
392
393pub async fn get_public_solution_submission_by_id(
395 pool: &PgPool,
396 solution_submission_id: &SolutionSubmissionId,
397) -> Result<Option<SolutionSubmissionRecord>> {
398 get_solution_submission_by_id_inner(pool, solution_submission_id, true).await
399}
400
401async fn get_solution_submission_by_id_inner(
404 pool: &PgPool,
405 solution_submission_id: &SolutionSubmissionId,
406 completed_official_only: bool,
407) -> Result<Option<SolutionSubmissionRecord>> {
408 let row = sqlx::query(
409 r#"
410 SELECT
411 s.id, s.challenge_name, s.target, s.agent_id,
412 p.title AS challenge_title, p.spec_json AS challenge_spec_json,
413 a.display_name AS agent_display_name,
414 s.artifact_key, s.note, s.status, s.explanation,
415 s.artifact_zip_bytes, s.artifact_uncompressed_bytes, s.artifact_file_count, s.artifact_sha256,
416 s.parent_solution_submission_id, s.credit_text, s.visible_after_eval,
417 s.created_at, s.updated_at,
418 j.id AS latest_job_id, j.status AS latest_job_status,
419 pe.id AS validation_eval_id,
420 pe.target AS validation_eval_target,
421 pe.status AS validation_eval_status,
422 pe.eval_type AS validation_eval_eval_type,
423 pe.aggregate_metrics_json AS validation_eval_aggregate_metrics,
424 pe.run_metrics_json AS validation_eval_run_metrics,
425 pe.public_results_json AS validation_eval_public_results,
426 pe.validation_summary_json AS validation_eval_validation_summary,
427 pe.official_summary_json AS validation_eval_official_summary,
428 pe.runner_log_storage_key AS validation_eval_runner_log_storage_key,
429 pe.started_at AS validation_eval_started_at,
430 pe.finished_at AS validation_eval_finished_at,
431 oe.id AS official_eval_id,
432 oe.target AS official_eval_target,
433 oe.status AS official_eval_status,
434 oe.eval_type AS official_eval_eval_type,
435 oe.aggregate_metrics_json AS official_eval_aggregate_metrics,
436 oe.run_metrics_json AS official_eval_run_metrics,
437 oe.public_results_json AS official_eval_public_results,
438 oe.validation_summary_json AS official_eval_validation_summary,
439 oe.official_summary_json AS official_eval_official_summary,
440 oe.runner_log_storage_key AS official_eval_runner_log_storage_key,
441 oe.started_at AS official_eval_started_at,
442 oe.finished_at AS official_eval_finished_at
443 FROM solution_submissions s
444 JOIN agents a ON a.id = s.agent_id
445 JOIN challenges p ON p.challenge_name = s.challenge_name
446 LEFT JOIN LATERAL (
447 SELECT id, status FROM evaluation_jobs WHERE solution_submission_id = s.id ORDER BY created_at DESC LIMIT 1
448 ) j ON TRUE
449 LEFT JOIN LATERAL (
450 SELECT id, target, status, eval_type, aggregate_metrics_json, run_metrics_json, public_results_json, validation_summary_json, official_summary_json, runner_log_storage_key, started_at, finished_at
451 FROM evaluations WHERE solution_submission_id = s.id AND eval_type = 'validation' AND target = s.target ORDER BY created_at DESC LIMIT 1
452 ) pe ON TRUE
453 LEFT JOIN LATERAL (
454 SELECT id, target, status, eval_type, aggregate_metrics_json, run_metrics_json, public_results_json, validation_summary_json, official_summary_json, runner_log_storage_key, started_at, finished_at
455 FROM evaluations
456 WHERE solution_submission_id = s.id
457 AND eval_type = 'official'
458 AND target = s.target
459 AND (NOT $2::boolean OR status = 'completed')
460 ORDER BY created_at DESC
461 LIMIT 1
462 ) oe ON TRUE
463 WHERE s.id = $1::uuid
464 LIMIT 1
465 "#
466 )
467 .bind(solution_submission_id.as_str())
468 .bind(completed_official_only)
469 .fetch_optional(pool)
470 .await?;
471
472 let Some(r) = row else {
473 return Ok(None);
474 };
475
476 let validation_eval = parse_eval_from_row(&r, "validation_eval")?;
477 let official_eval = parse_eval_from_row(&r, "official_eval")?;
478 let challenge_spec_json: Value = r.try_get("challenge_spec_json")?;
479 let challenge_spec = serde_json::from_value::<ChallengeBundleSpec>(challenge_spec_json)
480 .map_err(|e| ServiceError::Internal(format!("stored challenge spec is invalid: {e}")))?;
481
482 Ok(Some(SolutionSubmissionRecord {
483 id: solution_submission_id_from_row(&r, "id")?,
484 challenge_name: challenge_name_from_row(&r, "challenge_name")?,
485 target: target_from_row(&r, "target")?,
486 agent_id: agent_id_from_row(&r, "agent_id")?,
487 agent_display_name: r.try_get::<Option<String>, _>("agent_display_name")?,
488 challenge_title: r.try_get::<Option<String>, _>("challenge_title")?,
489 challenge_spec,
490 artifact_key: storage_key_from_row(&r, "artifact_key")?,
491 artifact_metadata: artifact_metadata_from_row(&r)?,
492 note: r.try_get("note")?,
493 status: r.try_get("status")?,
494 explanation: r.try_get("explanation")?,
495 parent_solution_submission_id: optional_solution_submission_id_from_row(
496 &r,
497 "parent_solution_submission_id",
498 )?,
499 credit_text: r.try_get("credit_text")?,
500 visible_after_eval: r.try_get("visible_after_eval")?,
501 created_at: r.try_get("created_at")?,
502 updated_at: r.try_get("updated_at")?,
503 evaluation_job_id: optional_evaluation_job_id_from_row(&r, "latest_job_id")?,
504 evaluation_job_status: r.try_get::<Option<String>, _>("latest_job_status")?,
505 evaluation: official_eval.clone().or_else(|| validation_eval.clone()),
506 validation_evaluation: validation_eval,
507 official_evaluation: official_eval,
508 }))
509}
510
511pub async fn list_admin_solution_submissions(
513 pool: &PgPool,
514 limit: i64,
515) -> Result<Vec<AdminSolutionSubmissionListItemRecord>> {
516 let rows = sqlx::query(
517 r#"
518 SELECT
519 s.id,
520 s.challenge_name,
521 s.target,
522 p.title AS challenge_title,
523 s.agent_id,
524 a.display_name AS agent_display_name,
525 s.note,
526 s.status,
527 s.visible_after_eval,
528 s.created_at,
529 s.updated_at,
530 j.id AS latest_job_id,
531 j.status AS latest_job_status,
532 j.eval_type AS latest_job_eval_type,
533 ve.status AS validation_status,
534 oe.status AS official_status
535 FROM solution_submissions s
536 JOIN challenges p ON p.challenge_name = s.challenge_name
537 JOIN agents a ON a.id = s.agent_id
538 LEFT JOIN LATERAL (
539 SELECT id, status, eval_type
540 FROM evaluation_jobs
541 WHERE solution_submission_id = s.id
542 ORDER BY created_at DESC
543 LIMIT 1
544 ) j ON TRUE
545 LEFT JOIN LATERAL (
546 SELECT status
547 FROM evaluations
548 WHERE solution_submission_id = s.id AND eval_type = 'validation'
549 ORDER BY created_at DESC
550 LIMIT 1
551 ) ve ON TRUE
552 LEFT JOIN LATERAL (
553 SELECT status
554 FROM evaluations
555 WHERE solution_submission_id = s.id AND eval_type = 'official'
556 ORDER BY created_at DESC
557 LIMIT 1
558 ) oe ON TRUE
559 ORDER BY s.updated_at DESC, s.created_at DESC
560 LIMIT $1
561 "#,
562 )
563 .bind(limit)
564 .fetch_all(pool)
565 .await?;
566
567 rows.into_iter()
568 .map(|r| {
569 Ok(AdminSolutionSubmissionListItemRecord {
570 id: solution_submission_id_from_row(&r, "id")?,
571 challenge_name: challenge_name_from_row(&r, "challenge_name")?,
572 challenge_title: r.try_get("challenge_title")?,
573 target: target_from_row(&r, "target")?,
574 agent_id: agent_id_from_row(&r, "agent_id")?,
575 agent_display_name: r.try_get("agent_display_name")?,
576 note: r.try_get("note")?,
577 status: solution_submission_status_from_row(&r, "status")?,
578 visible_after_eval: r.try_get("visible_after_eval")?,
579 latest_job_id: optional_evaluation_job_id_from_row(&r, "latest_job_id")?,
580 latest_job_status: optional_evaluation_job_status_from_row(
581 &r,
582 "latest_job_status",
583 )?,
584 latest_job_eval_type: optional_scoring_mode_from_row(&r, "latest_job_eval_type")?,
585 validation_status: optional_evaluation_status_from_row(&r, "validation_status")?,
586 official_status: optional_evaluation_status_from_row(&r, "official_status")?,
587 created_at: r.try_get("created_at")?,
588 updated_at: r.try_get("updated_at")?,
589 })
590 })
591 .collect::<Result<Vec<_>>>()
592}
593
594pub async fn list_public_solution_submissions_for_challenge(
596 pool: &PgPool,
597 challenge_name: &ChallengeName,
598 target: &TargetName,
599 limit: i64,
600) -> Result<Vec<PublicSolutionSubmissionListItemRecord>> {
601 let rows = sqlx::query(
602 r#"
603 SELECT
604 s.id, s.challenge_name, s.target, p.title AS challenge_title,
605 s.agent_id, a.display_name AS agent_display_name, s.status, s.note, s.explanation,
606 s.parent_solution_submission_id, s.credit_text, s.created_at, s.updated_at,
607 COALESCE(oe.aggregate_metrics_json, '[]'::jsonb) AS official_metrics
608 FROM solution_submissions s
609 JOIN agents a ON a.id = s.agent_id
610 JOIN challenges p ON p.challenge_name = s.challenge_name
611 LEFT JOIN LATERAL (
612 SELECT aggregate_metrics_json, official_summary_json
613 FROM evaluations
614 WHERE solution_submission_id = s.id AND eval_type = 'official' AND status = 'completed' AND target = s.target
615 ORDER BY created_at DESC LIMIT 1
616 ) oe ON TRUE
617 WHERE p.challenge_name = $1
618 AND s.visible_after_eval = TRUE
619 AND s.target = $2
620 ORDER BY s.created_at DESC
621 LIMIT $3
622 "#,
623 )
624 .bind(challenge_name.as_str())
625 .bind(target.as_str())
626 .bind(limit)
627 .fetch_all(pool)
628 .await?;
629
630 rows.into_iter()
631 .map(|r| {
632 let official_metrics: Vec<MetricValue> = decode_optional_json(
633 r.try_get::<Option<Value>, _>("official_metrics")?,
634 "solution submission official metrics",
635 )?
636 .unwrap_or_default();
637 Ok(PublicSolutionSubmissionListItemRecord {
638 id: solution_submission_id_from_row(&r, "id")?,
639 challenge_name: challenge_name_from_row(&r, "challenge_name")?,
640 target: target_from_row(&r, "target")?,
641 challenge_title: r.try_get("challenge_title")?,
642 agent_id: agent_id_from_row(&r, "agent_id")?,
643 agent_display_name: r.try_get("agent_display_name")?,
644 status: solution_submission_status_from_row(&r, "status")?,
645 note: r.try_get("note")?,
646 explanation: r.try_get("explanation")?,
647 parent_solution_submission_id: optional_solution_submission_id_from_row(
648 &r,
649 "parent_solution_submission_id",
650 )?,
651 credit_text: r.try_get("credit_text")?,
652 official_metrics,
653 created_at: r.try_get("created_at")?,
654 updated_at: r.try_get("updated_at")?,
655 })
656 })
657 .collect::<Result<Vec<_>>>()
658}
659
660pub async fn count_public_solution_submissions_for_challenge(
662 pool: &PgPool,
663 challenge_name: &ChallengeName,
664 target: &TargetName,
665) -> Result<i64> {
666 let count = sqlx::query_scalar::<_, i64>(
667 r#"
668 SELECT COUNT(*)::bigint
669 FROM solution_submissions s
670 WHERE s.challenge_name = $1
671 AND s.visible_after_eval = TRUE
672 AND s.target = $2
673 "#,
674 )
675 .bind(challenge_name.as_str())
676 .bind(target.as_str())
677 .fetch_one(pool)
678 .await?;
679
680 Ok(count)
681}
682
683pub async fn public_observer_stats(pool: &PgPool) -> Result<PublicObserverStatsRecord> {
685 let row = sqlx::query_as::<_, (i64, i64, i64, i64)>(
686 r#"
687 WITH public_challenges AS (
688 SELECT challenge_name, spec_json
689 FROM challenges
690 WHERE status = 'active'
691 AND spec_json IS NOT NULL
692 ),
693 public_submissions AS (
694 SELECT s.agent_id
695 FROM solution_submissions s
696 JOIN public_challenges c ON c.challenge_name = s.challenge_name
697 WHERE s.visible_after_eval = TRUE
698 AND s.status = 'completed'
699 AND (
700 c.spec_json #>> '{visibility,result_detail}' = 'submitter_live_public_live'
701 OR (
702 c.spec_json #>> '{visibility,result_detail}' = 'submitter_live_public_after_close'
703 AND (c.spec_json ->> 'closes_at')::timestamptz <= NOW()
704 )
705 )
706 ),
707 public_challenge_attempts AS (
708 SELECT s.id
709 FROM solution_submissions s
710 JOIN public_challenges c ON c.challenge_name = s.challenge_name
711 )
712 SELECT
713 (SELECT COUNT(*)::bigint FROM public_challenges) AS challenge_count,
714 (SELECT COUNT(DISTINCT agent_id)::bigint FROM public_submissions) AS agent_count,
715 (SELECT COUNT(*)::bigint FROM public_submissions) AS public_completed_submission_count,
716 (SELECT COUNT(*)::bigint FROM public_challenge_attempts) AS total_solution_attempt_count
717 "#,
718 )
719 .fetch_one(pool)
720 .await?;
721
722 Ok(PublicObserverStatsRecord {
723 challenge_count: count_to_u64("challenge_count", row.0)?,
724 agent_count: count_to_u64("agent_count", row.1)?,
725 public_completed_submission_count: count_to_u64(
726 "public_completed_submission_count",
727 row.2,
728 )?,
729 total_solution_attempt_count: count_to_u64("total_solution_attempt_count", row.3)?,
730 })
731}