Agent Builder: SQLite-taulu + REST API (GET/POST/DELETE)
- DB skeemaversio 3: agents-taulu (id, name, avatar, role, model, color, docs, prompt, temperature, top_k, max_tokens, repetition_penalty) - CRUD: upsert_agent, get_agents, delete_agent - API: GET/POST /api/v1/agents, DELETE /api/v1/agents/:id - Oletusagentteja (is_default=1) ei voi poistaa Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,29 @@ impl NodeDb {
|
|||||||
INSERT INTO _schema_version VALUES (2);
|
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("
|
conn.execute_batch("
|
||||||
CREATE TABLE IF NOT EXISTS node_sessions (
|
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<serde_json::Value> {
|
||||||
|
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<String>>(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(
|
pub fn insert_pair_result(
|
||||||
&self,
|
&self,
|
||||||
node_id: u64,
|
node_id: u64,
|
||||||
|
|||||||
@@ -387,6 +387,8 @@ async fn main() {
|
|||||||
.route("/api/v1/model", axum::routing::post(api_change_model))
|
.route("/api/v1/model", axum::routing::post(api_change_model))
|
||||||
.route("/api/v1/hardware", get(api_hardware))
|
.route("/api/v1/hardware", get(api_hardware))
|
||||||
.route("/api/v1/ollama/tags", get(api_ollama_tags))
|
.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))
|
.route("/admin", get(admin_page))
|
||||||
.nest_service("/", {
|
.nest_service("/", {
|
||||||
let static_dir = std::env::var("STATIC_DIR").unwrap_or_else(|_| "../static".to_string());
|
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()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Agents API ──
|
||||||
|
|
||||||
|
async fn api_get_agents(
|
||||||
|
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
|
||||||
|
) -> axum::response::Response {
|
||||||
|
axum::Json(state.db.get_agents()).into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn api_upsert_agent(
|
||||||
|
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
|
||||||
|
axum::Json(payload): axum::Json<serde_json::Value>,
|
||||||
|
) -> 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<Arc<AppState>>,
|
||||||
|
axum::extract::Path(id): axum::extract::Path<String>,
|
||||||
|
) -> 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 {
|
async fn admin_page(headers: axum::http::HeaderMap) -> axum::response::Response {
|
||||||
if !check_admin_auth(&headers) { return admin_unauthorized(); }
|
if !check_admin_auth(&headers) { return admin_unauthorized(); }
|
||||||
axum::response::Html(ADMIN_HTML).into_response()
|
axum::response::Html(ADMIN_HTML).into_response()
|
||||||
|
|||||||
Reference in New Issue
Block a user