feat: add POST /insights/generate/agentic handler and route

Register the agentic insight endpoint that validates tool-calling capability,
runs the agentic loop, and returns the stored PhotoInsightResponse. Returns 400
for unsupported models, 500 for other errors. Max iterations configurable via
AGENTIC_MAX_ITERATIONS env var (default 10).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Cameron
2026-03-18 23:01:25 -04:00
parent 7615b9c99b
commit 091327e5d9
3 changed files with 105 additions and 2 deletions

View File

@@ -211,6 +211,108 @@ pub async fn get_all_insights_handler(
}
}
/// POST /insights/generate/agentic - Generate insight using agentic tool-calling loop
#[post("/insights/generate/agentic")]
pub async fn generate_agentic_insight_handler(
http_request: HttpRequest,
_claims: Claims,
request: web::Json<GeneratePhotoInsightRequest>,
insight_generator: web::Data<InsightGenerator>,
insight_dao: web::Data<std::sync::Mutex<Box<dyn InsightDao>>>,
) -> impl Responder {
let parent_context = extract_context_from_request(&http_request);
let tracer = global_tracer();
let mut span = tracer.start_with_context("http.insights.generate_agentic", &parent_context);
let normalized_path = normalize_path(&request.file_path);
span.set_attribute(KeyValue::new("file_path", normalized_path.clone()));
if let Some(ref model) = request.model {
span.set_attribute(KeyValue::new("model", model.clone()));
}
if let Some(ref prompt) = request.system_prompt {
span.set_attribute(KeyValue::new("has_custom_prompt", true));
span.set_attribute(KeyValue::new("prompt_length", prompt.len() as i64));
}
if let Some(ctx) = request.num_ctx {
span.set_attribute(KeyValue::new("num_ctx", ctx as i64));
}
let max_iterations: usize = std::env::var("AGENTIC_MAX_ITERATIONS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(10);
span.set_attribute(KeyValue::new("max_iterations", max_iterations as i64));
log::info!(
"Agentic insight generation triggered for photo: {} with model: {:?}, max_iterations: {}",
normalized_path,
request.model,
max_iterations
);
let result = insight_generator
.generate_agentic_insight_for_photo(
&normalized_path,
request.model.clone(),
request.system_prompt.clone(),
request.num_ctx,
max_iterations,
)
.await;
match result {
Ok(()) => {
span.set_status(Status::Ok);
// Fetch the stored insight to return it
let otel_context = opentelemetry::Context::new();
let mut dao = insight_dao.lock().expect("Unable to lock InsightDao");
match dao.get_insight(&otel_context, &normalized_path) {
Ok(Some(insight)) => {
let response = PhotoInsightResponse {
id: insight.id,
file_path: insight.file_path,
title: insight.title,
summary: insight.summary,
generated_at: insight.generated_at,
model_version: insight.model_version,
};
HttpResponse::Ok().json(response)
}
Ok(None) => HttpResponse::Ok().json(serde_json::json!({
"success": true,
"message": "Agentic insight generated successfully"
})),
Err(e) => {
log::warn!("Insight stored but failed to retrieve: {:?}", e);
HttpResponse::Ok().json(serde_json::json!({
"success": true,
"message": "Agentic insight generated successfully"
}))
}
}
}
Err(e) => {
let error_msg = format!("{:?}", e);
log::error!("Failed to generate agentic insight: {}", error_msg);
span.set_status(Status::error(error_msg.clone()));
if error_msg.contains("tool calling not supported")
|| error_msg.contains("model not available")
{
HttpResponse::BadRequest().json(serde_json::json!({
"error": format!("Failed to generate agentic insight: {}", error_msg)
}))
} else {
HttpResponse::InternalServerError().json(serde_json::json!({
"error": format!("Failed to generate agentic insight: {}", error_msg)
}))
}
}
}
}
/// GET /insights/models - List available models from both servers with capabilities
#[get("/insights/models")]
pub async fn get_available_models_handler(

View File

@@ -8,8 +8,8 @@ pub mod sms_client;
#[allow(unused_imports)]
pub use daily_summary_job::{generate_daily_summaries, strip_summary_boilerplate};
pub use handlers::{
delete_insight_handler, generate_insight_handler, get_all_insights_handler,
get_available_models_handler, get_insight_handler,
delete_insight_handler, generate_agentic_insight_handler, generate_insight_handler,
get_all_insights_handler, get_available_models_handler, get_insight_handler,
};
pub use insight_generator::InsightGenerator;
pub use ollama::{ModelCapabilities, OllamaClient};

View File

@@ -1189,6 +1189,7 @@ fn main() -> std::io::Result<()> {
.service(get_file_metadata)
.service(memories::list_memories)
.service(ai::generate_insight_handler)
.service(ai::generate_agentic_insight_handler)
.service(ai::get_insight_handler)
.service(ai::delete_insight_handler)
.service(ai::get_all_insights_handler)