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:
@@ -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(
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user