Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
5.6 kB · 178 lines
Rust
at master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179use crate::{error::ApiError, state::AppState};use axum::{ Json, extract::{Path, State},};use serde::Deserialize;use serde_json::{Value, json};use uuid::Uuid;
pub async fn capabilities() -> Json<Value> { Json(json!({ "api_version": "v1", "entities": { "problem": "A task with a prompt, owner-authored solution and tags.", "shelf_item": "A link or resource collected for learning.", "learning_space": "A course or a short learning session.", "lesson": "A structured lesson that can reference problems and shelf items." }, "reader": { "materials": "/api/reader/materials", "import": "/api/reader/import", "authoring_rules": "/api/reader/authoring-rules", "content_format": "Markdown with math, Mermaid, source citations and stable exercise IDs", "guide": "AGENT_GUIDE.md" } }))}
#[derive(Debug, Deserialize)]pub struct AgentProblemRequest { pub title: String, pub prompt: Option<Value>, pub tags: Option<Vec<String>>, pub source_context: Option<Value>,}
pub async fn create_problem( State(state): State<AppState>, Json(input): Json<AgentProblemRequest>,) -> Result<Json<Value>, ApiError> { let row: Value = sqlx::query_scalar( "insert into problems (title, prompt, tags, source_context, created_by) values ($1, $2, $3, $4, 'agent') returning to_jsonb(problems.*)", ) .bind(required_title(&input.title)?) .bind(input.prompt.unwrap_or_else(empty_document)) .bind(normalize_tags(input.tags.unwrap_or_default())) .bind(input.source_context) .fetch_one(&state.platform_db) .await?; Ok(Json(row))}
#[derive(Debug, Deserialize)]pub struct AgentShelfItemRequest { pub collection_id: Option<Uuid>, pub kind: String, pub title: String, pub url: Option<String>, pub asset_id: Option<Uuid>, pub tags: Option<Vec<String>>, pub metadata: Option<Value>,}
pub async fn create_shelf_item( State(state): State<AppState>, Json(input): Json<AgentShelfItemRequest>,) -> Result<Json<Value>, ApiError> { if !matches!( input.kind.as_str(), "article" | "book" | "file" | "link" | "image" | "video" ) { return Err(ApiError::BadRequest("unsupported shelf item kind".into())); } let row: Value = sqlx::query_scalar( "insert into shelf_items (collection_id, kind, title, url, asset_id, tags, metadata, created_by) values ($1, $2, $3, $4, $5, $6, $7, 'agent') returning to_jsonb(shelf_items.*)", ) .bind(input.collection_id) .bind(input.kind) .bind(required_title(&input.title)?) .bind(input.url) .bind(input.asset_id) .bind(normalize_tags(input.tags.unwrap_or_default())) .bind(input.metadata.unwrap_or_else(|| json!({}))) .fetch_one(&state.platform_db) .await?; Ok(Json(row))}
#[derive(Debug, Deserialize)]pub struct AgentLearningSpaceRequest { pub kind: String, pub title: String, pub description: Option<String>, pub metadata: Option<Value>,}
pub async fn create_learning_space( State(state): State<AppState>, Json(input): Json<AgentLearningSpaceRequest>,) -> Result<Json<Value>, ApiError> { if input.kind != "course" && input.kind != "session" { return Err(ApiError::BadRequest( "kind must be course or session".into(), )); } let row: Value = sqlx::query_scalar( "insert into learning_spaces (kind, title, description, metadata) values ($1, $2, $3, $4) returning to_jsonb(learning_spaces.*)", ) .bind(input.kind) .bind(required_title(&input.title)?) .bind(input.description.unwrap_or_default()) .bind(input.metadata.unwrap_or_else(|| json!({}))) .fetch_one(&state.platform_db) .await?; Ok(Json(row))}
#[derive(Debug, Deserialize)]pub struct AgentLessonRequest { pub title: String, pub position: Option<i32>, pub document: Option<Value>, pub resource_ids: Option<Vec<Uuid>>, pub problem_ids: Option<Vec<Uuid>>,}
pub async fn create_lesson( State(state): State<AppState>, Path(space_id): Path<Uuid>, Json(input): Json<AgentLessonRequest>,) -> Result<Json<Value>, ApiError> { let row: Value = sqlx::query_scalar( "insert into learning_lessons (space_id, position, title, document, resource_ids, problem_ids, created_by) values ($1, $2, $3, $4, $5, $6, 'agent') returning to_jsonb(learning_lessons.*)", ) .bind(space_id) .bind(input.position.unwrap_or(0)) .bind(required_title(&input.title)?) .bind(input.document.unwrap_or_else(empty_document)) .bind(input.resource_ids.unwrap_or_default()) .bind(input.problem_ids.unwrap_or_default()) .fetch_one(&state.platform_db) .await?; Ok(Json(row))}
fn required_title(value: &str) -> Result<&str, ApiError> { let title = value.trim(); if title.is_empty() { Err(ApiError::BadRequest("title is required".into())) } else { Ok(title) }}
fn normalize_tags(tags: Vec<String>) -> Vec<String> { let mut tags: Vec<_> = tags .into_iter() .map(|tag| tag.trim().to_owned()) .filter(|tag| !tag.is_empty()) .collect(); tags.sort_by_key(|tag| tag.to_lowercase()); tags.dedup_by(|left, right| left.eq_ignore_ascii_case(right)); tags}
fn empty_document() -> Value { json!({"schema_version": 1, "content": {"type": "doc", "content": []}})}