CI / CI (push) Successful in 11m36s
## Problem 8 integration test files failed to compile due to: 1. Ambiguous float types (Rust 2024+ stricter inference) 2. chrono 0.4 API change (`with_hour` removed) 3. Missing `sqlx` + `base64` in `[dev-dependencies]` 4. `<` parsed as generics instead of comparison 5. Incorrect assertion (3^5=243 > 100) ## Fix - Added `f32`/`f64` type annotations to vec declarations and bindings - Replaced `with_hour(0)` with `date_naive().and_hms_opt(0,0,0).unwrap().and_utc()` - Added `sqlx` + `base64` to `[dev-dependencies]` - Wrapped comparison in parens - Fixed assertion: nodes=100 → nodes=1000 ## Validation - `cargo build --release` clean - `cargo test` — 20 test suites, 0 failures - 10 files changed, 46 insertions, 42 deletionsReviewed-on: #46 Co-authored-by: rock <[email protected]>
320 lines
10 KiB
Rust
320 lines
10 KiB
Rust
//! Agent Lifecycle Handlers (Phase 6)
|
|
|
|
use actix_web::{web, HttpRequest, HttpResponse};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::sync::Arc;
|
|
use crate::agent::{Agent, AgentConfig, AgentCapability, DefaultAgent};
|
|
use crate::agent::client_sdk::SynthesisClient;
|
|
use crate::handlers::response_builder;
|
|
use tracing::{debug, info, error, warn};
|
|
|
|
/// Register agent request
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct RegisterAgentRequest {
|
|
pub agent_id: String,
|
|
pub project_id: String,
|
|
pub capabilities: Vec<String>,
|
|
pub webhook_url: Option<String>,
|
|
pub rate_limit: Option<u32>,
|
|
}
|
|
|
|
/// Agent response
|
|
#[derive(Debug, Serialize)]
|
|
pub struct AgentResponse {
|
|
pub agent_id: String,
|
|
pub project_id: String,
|
|
pub capabilities: Vec<String>,
|
|
pub webhook_url: Option<String>,
|
|
pub rate_limit: u32,
|
|
pub created_at: String,
|
|
pub status: String,
|
|
}
|
|
|
|
// JWT token extraction is now in crate::handlers::jwt_utils
|
|
|
|
/// POST /agents - Register new agent
|
|
pub async fn register_agent_handler(
|
|
req: HttpRequest,
|
|
body: web::Json<RegisterAgentRequest>,
|
|
state: web::Data<crate::AppState>,
|
|
) -> HttpResponse {
|
|
if let Err(response) = crate::handlers::middleware::validate_and_rate_limit(
|
|
&req, &state, "agent", 50
|
|
) {
|
|
return response;
|
|
}
|
|
|
|
if body.agent_id.is_empty() || body.project_id.is_empty() {
|
|
return response_builder::bad_request("agent_id and project_id required");
|
|
}
|
|
|
|
if body.capabilities.is_empty() {
|
|
return response_builder::bad_request("At least one capability required");
|
|
}
|
|
|
|
debug!("Registering agent: {}", body.agent_id);
|
|
|
|
// Parse capabilities
|
|
let caps: Vec<AgentCapability> = body.capabilities.iter()
|
|
.filter_map(|c| match c.as_str() {
|
|
"entity_linking" => Some(AgentCapability::EntityLinking),
|
|
"inference_facts" => Some(AgentCapability::InferenceFacts),
|
|
"reason_query" => Some(AgentCapability::ReasonQuery),
|
|
"summarization" => Some(AgentCapability::Summarization),
|
|
"semantic_search" => Some(AgentCapability::SemanticSearch),
|
|
"graph_traversal" => Some(AgentCapability::GraphTraversal),
|
|
_ => None,
|
|
})
|
|
.collect();
|
|
|
|
if caps.is_empty() {
|
|
return response_builder::bad_request("Invalid capabilities");
|
|
}
|
|
|
|
let config = AgentConfig {
|
|
agent_id: body.agent_id.clone(),
|
|
project_id: body.project_id.clone(),
|
|
capabilities: caps.clone(),
|
|
webhook_url: body.webhook_url.clone(),
|
|
rate_limit: body.rate_limit.unwrap_or(1000),
|
|
metadata: std::collections::HashMap::new(),
|
|
};
|
|
|
|
// Store agent config (stub: would persist to DB)
|
|
let agent = DefaultAgent::new(config);
|
|
|
|
// Extract JWT from request for agent reasoning calls
|
|
if let Some(jwt) = crate::handlers::extract_jwt_token(&req) {
|
|
debug!("Agent registered with JWT token (len: {})", jwt.len());
|
|
} else {
|
|
warn!("Agent registered without JWT token");
|
|
}
|
|
|
|
info!("Agent registered: {}", agent.config().agent_id);
|
|
|
|
// Wire Temporal workflow (via api.riotpiao.com/workflow)
|
|
// Temporal activities will:
|
|
// 1. Persist agent state to temporal_workflow_links table
|
|
// 2. Execute LLMInferenceActivity (call LLM via api.riotpiao.com/v1/chat/completions)
|
|
// 3. Store reasoning traces to memory_entity/memory_edge
|
|
if let Some(jwt) = crate::handlers::extract_jwt_token(&req) {
|
|
let client = SynthesisClient::new(
|
|
"https://api.riotpiao.com".to_string(),
|
|
jwt,
|
|
);
|
|
|
|
// Start Temporal workflow for agent initialization
|
|
// Include LLMInferenceActivity configuration for capability verification
|
|
let workflow_input = serde_json::json!({
|
|
"agent_id": body.agent_id,
|
|
"capabilities": body.capabilities,
|
|
"project_id": body.project_id,
|
|
|
|
// LLMInferenceActivity inputs for agent capability reasoning
|
|
"llm_activity": {
|
|
"model": "ornith:13b",
|
|
"system_prompt": "You are an agent capability validator. Verify that the requested capabilities are valid for the memory system. Return JSON with 'valid' boolean and 'reason' string.",
|
|
"user_prompt": format!("Validate agent capabilities: {:?}", body.capabilities),
|
|
"temperature": 0.5,
|
|
"max_tokens": 512
|
|
}
|
|
});
|
|
|
|
let workflow_req = crate::handlers::WorkflowBuilder::new("AgentInitialization")
|
|
.with_id(&format!("agent-init-{}", body.agent_id))
|
|
.with_queue("agents")
|
|
.with_input(workflow_input)
|
|
.build();
|
|
|
|
match tokio::runtime::Handle::current().block_on(client.execute_workflow(workflow_req)) {
|
|
Ok(response) => {
|
|
if let Some(data) = response.get("data") {
|
|
let workflow_id = data.get("workflow_id").and_then(|v| v.as_str()).unwrap_or("unknown");
|
|
let run_id = data.get("run_id").and_then(|v| v.as_str()).unwrap_or("unknown");
|
|
|
|
// Store workflow reference in temporal_workflow_links
|
|
// (DB insert would happen here in production)
|
|
info!("Agent workflow started: workflow_id={}, run_id={}", workflow_id, run_id);
|
|
debug!("Temporal activity will persist agent state + reasoning traces");
|
|
}
|
|
}
|
|
Err(e) => {
|
|
warn!("Failed to start agent workflow: {}", e);
|
|
// Non-fatal: agent still created, just workflow unavailable
|
|
}
|
|
}
|
|
}
|
|
|
|
response_builder::success_response(AgentResponse {
|
|
agent_id: agent.config().agent_id.clone(),
|
|
project_id: agent.config().project_id.clone(),
|
|
capabilities: body.capabilities.clone(),
|
|
webhook_url: body.webhook_url.clone(),
|
|
rate_limit: agent.config().rate_limit,
|
|
created_at: chrono::Utc::now().to_rfc3339(),
|
|
status: "active".to_string(),
|
|
})
|
|
}
|
|
|
|
/// GET /agents/{id} - Get agent status
|
|
pub async fn get_agent_handler(
|
|
req: HttpRequest,
|
|
path: web::Path<String>,
|
|
state: web::Data<crate::AppState>,
|
|
) -> HttpResponse {
|
|
let agent_id = path.into_inner();
|
|
|
|
if let Err(response) = crate::handlers::middleware::validate_and_rate_limit(
|
|
&req, &state, "agent", 100
|
|
) {
|
|
return response;
|
|
}
|
|
|
|
debug!("Getting agent: {}", agent_id);
|
|
|
|
// Extract JWT for agent operations
|
|
let jwt = crate::handlers::extract_jwt_token(&req)
|
|
.unwrap_or_else(|| {
|
|
warn!("No JWT token in get_agent request");
|
|
"invalid".to_string()
|
|
});
|
|
|
|
// Stub: would fetch from DB
|
|
let config = AgentConfig {
|
|
agent_id: agent_id.clone(),
|
|
project_id: "poimen".to_string(),
|
|
capabilities: vec![AgentCapability::Summarization],
|
|
webhook_url: None,
|
|
rate_limit: 1000,
|
|
metadata: std::collections::HashMap::new(),
|
|
};
|
|
|
|
let agent = DefaultAgent::new(config);
|
|
|
|
match futures::executor::block_on(agent.status()) {
|
|
status => {
|
|
info!("Agent status: {} with JWT auth", agent_id);
|
|
response_builder::success_response(status)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Metrics response
|
|
#[derive(Debug, Serialize)]
|
|
pub struct MetricsResponse {
|
|
pub agent_id: String,
|
|
pub requests_total: u64,
|
|
pub requests_success: u64,
|
|
pub requests_failed: u64,
|
|
pub average_latency_ms: f32,
|
|
pub p95_latency_ms: f32,
|
|
pub p99_latency_ms: f32,
|
|
pub error_rate: f32,
|
|
}
|
|
|
|
/// GET /agents/{id}/metrics - Get agent metrics
|
|
pub async fn get_agent_metrics_handler(
|
|
req: HttpRequest,
|
|
path: web::Path<String>,
|
|
state: web::Data<crate::AppState>,
|
|
) -> HttpResponse {
|
|
let agent_id = path.into_inner();
|
|
|
|
if let Err(response) = crate::handlers::middleware::validate_and_rate_limit(
|
|
&req, &state, "agent", 100
|
|
) {
|
|
return response;
|
|
}
|
|
|
|
debug!("Getting metrics for agent: {}", agent_id);
|
|
|
|
// Extract JWT token for all agent metric operations
|
|
if let Some(jwt) = crate::handlers::extract_jwt_token(&req) {
|
|
debug!("Metrics request authenticated with JWT (len: {})", jwt.len());
|
|
}
|
|
|
|
// Stub: would fetch from metrics store
|
|
let error_rate = if 0 == 0 { 0.0 } else { 0.05 };
|
|
|
|
let metrics = MetricsResponse {
|
|
agent_id: agent_id.clone(),
|
|
requests_total: 1000,
|
|
requests_success: 950,
|
|
requests_failed: 50,
|
|
average_latency_ms: 145.5,
|
|
p95_latency_ms: 310.0,
|
|
p99_latency_ms: 450.0,
|
|
error_rate,
|
|
};
|
|
|
|
info!("Retrieved metrics for agent: {}", agent_id);
|
|
response_builder::success_response(metrics)
|
|
}
|
|
|
|
/// PUT /agents/{id} - Update agent config
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct UpdateAgentRequest {
|
|
pub webhook_url: Option<String>,
|
|
pub rate_limit: Option<u32>,
|
|
pub capabilities: Option<Vec<String>>,
|
|
}
|
|
|
|
pub async fn update_agent_handler(
|
|
req: HttpRequest,
|
|
path: web::Path<String>,
|
|
body: web::Json<UpdateAgentRequest>,
|
|
state: web::Data<crate::AppState>,
|
|
) -> HttpResponse {
|
|
let agent_id = path.into_inner();
|
|
|
|
if let Err(response) = crate::handlers::middleware::validate_and_rate_limit(
|
|
&req, &state, "agent", 50
|
|
) {
|
|
return response;
|
|
}
|
|
|
|
debug!("Updating agent: {}", agent_id);
|
|
|
|
// Verify JWT present for update operations
|
|
if crate::handlers::extract_jwt_token(&req).is_none() {
|
|
warn!("Update request for {} without JWT", agent_id);
|
|
}
|
|
|
|
// Stub: would update in DB
|
|
response_builder::success_response(serde_json::json!({
|
|
"agent_id": agent_id,
|
|
"updated": true,
|
|
"webhook_url": body.webhook_url,
|
|
"rate_limit": body.rate_limit,
|
|
}))
|
|
}
|
|
|
|
/// DELETE /agents/{id} - Deregister agent
|
|
pub async fn delete_agent_handler(
|
|
req: HttpRequest,
|
|
path: web::Path<String>,
|
|
state: web::Data<crate::AppState>,
|
|
) -> HttpResponse {
|
|
let agent_id = path.into_inner();
|
|
|
|
if let Err(response) = crate::handlers::middleware::validate_and_rate_limit(
|
|
&req, &state, "agent", 50
|
|
) {
|
|
return response;
|
|
}
|
|
|
|
debug!("Deregistering agent: {}", agent_id);
|
|
|
|
// Require JWT for deletion (security)
|
|
if crate::handlers::extract_jwt_token(&req).is_none() {
|
|
return response_builder::unauthorized("JWT token required for agent deletion");
|
|
}
|
|
|
|
info!("Agent deregistered: {} with JWT auth", agent_id);
|
|
response_builder::success_response(serde_json::json!({
|
|
"agent_id": agent_id,
|
|
"deregistered": true,
|
|
}))
|
|
}
|
|
|