diff --git a/network-poc/hub/src/db.rs b/network-poc/hub/src/db.rs index e3f1b77..fd4058b 100644 --- a/network-poc/hub/src/db.rs +++ b/network-poc/hub/src/db.rs @@ -26,6 +26,29 @@ impl NodeDb { INSERT INTO _schema_version VALUES (2); "); } + if version < 3 { + let _ = conn.execute_batch(" + CREATE TABLE IF NOT EXISTS agents ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + avatar TEXT NOT NULL DEFAULT '/avatars/kipina_notext.png', + role TEXT NOT NULL DEFAULT 'coder', + model TEXT NOT NULL DEFAULT 'qwen2.5-coder:7b', + color TEXT NOT NULL DEFAULT '#3fb950', + docs TEXT, + prompt TEXT NOT NULL DEFAULT '', + temperature REAL DEFAULT 0.7, + top_k INTEGER DEFAULT 40, + max_tokens INTEGER DEFAULT 512, + repetition_penalty REAL DEFAULT 1.15, + is_default BOOLEAN DEFAULT 0, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + ); + DELETE FROM _schema_version; + INSERT INTO _schema_version VALUES (3); + "); + } conn.execute_batch(" CREATE TABLE IF NOT EXISTS node_sessions ( @@ -279,6 +302,82 @@ impl NodeDb { }) } + // ── Agents CRUD ── + + pub fn upsert_agent(&self, agent: &serde_json::Value) -> Result<(), String> { + let conn = self.conn.lock().unwrap_or_else(|e| e.into_inner()); + let now = chrono::Utc::now().to_rfc3339(); + let id = agent.get("id").and_then(|v| v.as_str()).ok_or("id puuttuu")?; + let name = agent.get("name").and_then(|v| v.as_str()).ok_or("name puuttuu")?; + conn.execute( + "INSERT INTO agents (id, name, avatar, role, model, color, docs, prompt, + temperature, top_k, max_tokens, repetition_penalty, is_default, created_at, updated_at) + VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?14) + ON CONFLICT(id) DO UPDATE SET + name=?2, avatar=?3, role=?4, model=?5, color=?6, docs=?7, prompt=?8, + temperature=?9, top_k=?10, max_tokens=?11, repetition_penalty=?12, updated_at=?14", + params![ + id, name, + agent.get("avatar").and_then(|v| v.as_str()).unwrap_or("/avatars/kipina_notext.png"), + agent.get("role").and_then(|v| v.as_str()).unwrap_or("coder"), + agent.get("model").and_then(|v| v.as_str()).unwrap_or("qwen2.5-coder:7b"), + agent.get("color").and_then(|v| v.as_str()).unwrap_or("#3fb950"), + agent.get("docs").and_then(|v| v.as_str()), + agent.get("prompt").and_then(|v| v.as_str()).unwrap_or(""), + agent.get("temperature").and_then(|v| v.as_f64()).unwrap_or(0.7), + agent.get("top_k").and_then(|v| v.as_u64()).unwrap_or(40) as i64, + agent.get("max_tokens").and_then(|v| v.as_u64()).unwrap_or(512) as i64, + agent.get("repetition_penalty").and_then(|v| v.as_f64()).unwrap_or(1.15), + agent.get("is_default").and_then(|v| v.as_bool()).unwrap_or(false), + now, + ], + ).map_err(|e| format!("Agent upsert: {}", e))?; + Ok(()) + } + + pub fn get_agents(&self) -> Vec { + let conn = self.conn.lock().unwrap_or_else(|e| e.into_inner()); + let mut stmt = conn.prepare( + "SELECT id, name, avatar, role, model, color, docs, prompt, + temperature, top_k, max_tokens, repetition_penalty, is_default, + created_at, updated_at + FROM agents ORDER BY is_default DESC, name" + ).unwrap(); + + stmt.query_map([], |row| { + Ok(serde_json::json!({ + "id": row.get::<_, String>(0)?, + "name": row.get::<_, String>(1)?, + "avatar": row.get::<_, String>(2)?, + "role": row.get::<_, String>(3)?, + "model": row.get::<_, String>(4)?, + "color": row.get::<_, String>(5)?, + "docs": row.get::<_, Option>(6)?, + "prompt": row.get::<_, String>(7)?, + "temperature": row.get::<_, f64>(8)?, + "top_k": row.get::<_, i64>(9)?, + "max_tokens": row.get::<_, i64>(10)?, + "repetition_penalty": row.get::<_, f64>(11)?, + "is_default": row.get::<_, bool>(12)?, + "created_at": row.get::<_, String>(13)?, + "updated_at": row.get::<_, String>(14)?, + })) + }).unwrap().filter_map(|r| r.ok()).collect() + } + + pub fn delete_agent(&self, id: &str) -> Result<(), String> { + let conn = self.conn.lock().unwrap_or_else(|e| e.into_inner()); + let deleted = conn.execute( + "DELETE FROM agents WHERE id = ?1 AND is_default = 0", + params![id], + ).map_err(|e| format!("Agent delete: {}", e))?; + if deleted == 0 { + Err("Agenttia ei löydy tai se on oletusagentti".to_string()) + } else { + Ok(()) + } + } + pub fn insert_pair_result( &self, node_id: u64, diff --git a/network-poc/hub/src/main.rs b/network-poc/hub/src/main.rs index e5233dc..92783de 100644 --- a/network-poc/hub/src/main.rs +++ b/network-poc/hub/src/main.rs @@ -387,6 +387,8 @@ async fn main() { .route("/api/v1/model", axum::routing::post(api_change_model)) .route("/api/v1/hardware", get(api_hardware)) .route("/api/v1/ollama/tags", get(api_ollama_tags)) + .route("/api/v1/agents", get(api_get_agents).post(api_upsert_agent)) + .route("/api/v1/agents/:id", axum::routing::delete(api_delete_agent)) .route("/admin", get(admin_page)) .nest_service("/", { let static_dir = std::env::var("STATIC_DIR").unwrap_or_else(|_| "../static".to_string()); @@ -462,6 +464,34 @@ fn admin_unauthorized() -> axum::response::Response { .unwrap() } +// ── Agents API ── + +async fn api_get_agents( + axum::extract::State(state): axum::extract::State>, +) -> axum::response::Response { + axum::Json(state.db.get_agents()).into_response() +} + +async fn api_upsert_agent( + axum::extract::State(state): axum::extract::State>, + axum::Json(payload): axum::Json, +) -> axum::response::Response { + match state.db.upsert_agent(&payload) { + Ok(()) => axum::Json(serde_json::json!({"ok": true})).into_response(), + Err(e) => (axum::http::StatusCode::BAD_REQUEST, e).into_response(), + } +} + +async fn api_delete_agent( + axum::extract::State(state): axum::extract::State>, + axum::extract::Path(id): axum::extract::Path, +) -> axum::response::Response { + match state.db.delete_agent(&id) { + Ok(()) => axum::Json(serde_json::json!({"ok": true})).into_response(), + Err(e) => (axum::http::StatusCode::BAD_REQUEST, e).into_response(), + } +} + async fn admin_page(headers: axum::http::HeaderMap) -> axum::response::Response { if !check_admin_auth(&headers) { return admin_unauthorized(); } axum::response::Html(ADMIN_HTML).into_response()