//! ToolInvoker app-tauri pour l'adapter OpenAI-compatible. //! //! C'est la porte locale qui donne aux modèles HTTP la même surface `idea_*` que //! le serveur MCP : même catalogue, même mapping en `OrchestratorCommand`, même //! `OrchestratorService::dispatch`. use std::sync::{Arc, Mutex}; use application::OrchestratorService; use async_trait::async_trait; use domain::ports::{ProjectStore, ToolInvocationError, ToolInvoker, ToolSpec}; use serde_json::Value; const PROJECT_ROOT_ARG: &str = "__ideaProjectRoot"; const REQUESTER_ARG: &str = "__ideaRequester"; /// Invoker d'outils OpenAI-compatible branché sur l'orchestrateur applicatif. pub struct AppOpenAiToolInvoker { orchestrator: Arc, projects: Arc, } /// Proxy injecté avant que l'orchestrateur soit construit, puis lié dans la /// composition root. Il casse uniquement le cycle de wiring, pas le contrat runtime. #[derive(Default)] pub struct LateBoundOpenAiToolInvoker { inner: Mutex>>, } impl LateBoundOpenAiToolInvoker { /// Construit un proxy vide. #[must_use] pub fn new() -> Self { Self { inner: Mutex::new(None), } } /// Lie l'implémentation réelle. Appelé une fois par la composition root. pub fn bind(&self, inner: Arc) { *self.inner.lock().expect("mutex sain") = Some(inner); } } #[async_trait] impl ToolInvoker for LateBoundOpenAiToolInvoker { fn tools(&self) -> Vec { self.inner .lock() .expect("mutex sain") .as_ref() .map_or_else(Vec::new, |inner| inner.tools()) } async fn call(&self, name: &str, args_json: &str) -> Result { let inner = self .inner .lock() .expect("mutex sain") .clone() .ok_or_else(|| { ToolInvocationError::Execution("ToolInvoker OpenAI non initialisé".to_owned()) })?; inner.call(name, args_json).await } } impl AppOpenAiToolInvoker { /// Construit l'invoker depuis le service orchestrateur et le store projet. #[must_use] pub fn new(orchestrator: Arc, projects: Arc) -> Self { Self { orchestrator, projects, } } } #[async_trait] impl ToolInvoker for AppOpenAiToolInvoker { fn tools(&self) -> Vec { infrastructure::orchestrator::mcp::catalogue() .into_iter() .map(|tool| ToolSpec { name: tool.name.to_owned(), description: tool.description.to_owned(), input_schema: tool.input_schema, }) .collect() } async fn call(&self, name: &str, args_json: &str) -> Result { let value: Value = serde_json::from_str(args_json) .map_err(|e| ToolInvocationError::InvalidArguments(format!("JSON invalide: {e}")))?; let args = value.as_object().ok_or_else(|| { ToolInvocationError::InvalidArguments( "les arguments d'outil doivent être un objet JSON".to_owned(), ) })?; let project_root = args .get(PROJECT_ROOT_ARG) .and_then(Value::as_str) .ok_or_else(|| { ToolInvocationError::InvalidArguments( "contexte projet interne absent pour l'outil".to_owned(), ) })?; let requester = args .get(REQUESTER_ARG) .and_then(Value::as_str) .ok_or_else(|| { ToolInvocationError::InvalidArguments( "identité requester interne absente pour l'outil".to_owned(), ) })?; let project = self .projects .list_projects() .await .map_err(|e| ToolInvocationError::Execution(e.to_string()))? .into_iter() .find(|project| project.root.as_str() == project_root) .ok_or_else(|| { ToolInvocationError::Execution(format!( "projet introuvable pour root `{project_root}`" )) })?; let command = infrastructure::orchestrator::mcp::map_tool_call(name, &value, requester) .map_err(|e| match e { infrastructure::orchestrator::mcp::ToolMapError::UnknownTool(tool) => { ToolInvocationError::NotFound(tool) } infrastructure::orchestrator::mcp::ToolMapError::BadArguments(tool) => { ToolInvocationError::InvalidArguments(format!( "arguments invalides pour `{tool}`" )) } infrastructure::orchestrator::mcp::ToolMapError::Invalid(err) => { ToolInvocationError::InvalidArguments(err.to_string()) } })?; let outcome = self .orchestrator .dispatch(&project, command) .await .map_err(|e| ToolInvocationError::Execution(e.to_string()))?; Ok(outcome.reply.unwrap_or(outcome.detail)) } }