From 7f987737f987e43b421db5660271d73358431069 Mon Sep 17 00:00:00 2001 From: vikingowl Date: Fri, 17 Oct 2025 01:52:10 +0200 Subject: [PATCH] refactor(core): add LLMClient facade trait; decouple TUI from Provider/MCP details --- crates/owlen-core/src/facade/llm_client.rs | 32 ++++++++++++++++++++ crates/owlen-core/src/facade/mod.rs | 1 + crates/owlen-core/src/lib.rs | 2 ++ crates/owlen-core/src/mcp/remote_client.rs | 29 +++++++++++++++++- crates/owlen-tui/src/chat_app.rs | 34 ++++++++++++---------- 5 files changed, 82 insertions(+), 16 deletions(-) create mode 100644 crates/owlen-core/src/facade/llm_client.rs create mode 100644 crates/owlen-core/src/facade/mod.rs diff --git a/crates/owlen-core/src/facade/llm_client.rs b/crates/owlen-core/src/facade/llm_client.rs new file mode 100644 index 0000000..2c64f2d --- /dev/null +++ b/crates/owlen-core/src/facade/llm_client.rs @@ -0,0 +1,32 @@ +use std::sync::Arc; + +use async_trait::async_trait; + +use crate::{ + Result, + llm::ChatStream, + mcp::{McpToolCall, McpToolDescriptor, McpToolResponse}, + types::{ChatRequest, ChatResponse, ModelInfo}, +}; + +/// Object-safe facade for interacting with LLM backends. +#[async_trait] +pub trait LlmClient: Send + Sync { + /// List the models exposed by this client. + async fn list_models(&self) -> Result>; + + /// Issue a one-shot chat request and wait for the complete response. + async fn send_chat(&self, request: ChatRequest) -> Result; + + /// Stream chat responses incrementally. + async fn stream_chat(&self, request: ChatRequest) -> Result; + + /// Enumerate tools exposed by the backing provider. + async fn list_tools(&self) -> Result>; + + /// Invoke a tool exposed by the provider. + async fn call_tool(&self, call: McpToolCall) -> Result; +} + +/// Convenience alias for trait-object clients. +pub type DynLlmClient = Arc; diff --git a/crates/owlen-core/src/facade/mod.rs b/crates/owlen-core/src/facade/mod.rs new file mode 100644 index 0000000..23952d0 --- /dev/null +++ b/crates/owlen-core/src/facade/mod.rs @@ -0,0 +1 @@ +pub mod llm_client; diff --git a/crates/owlen-core/src/lib.rs b/crates/owlen-core/src/lib.rs index 831a8e4..3a1c574 100644 --- a/crates/owlen-core/src/lib.rs +++ b/crates/owlen-core/src/lib.rs @@ -11,6 +11,7 @@ pub mod consent; pub mod conversation; pub mod credentials; pub mod encryption; +pub mod facade; pub mod formatting; pub mod input; pub mod llm; @@ -42,6 +43,7 @@ pub use formatting::*; pub use input::*; pub use oauth::*; // Export MCP types but exclude test_utils to avoid ambiguity +pub use facade::llm_client::*; pub use llm::{ ChatStream, LlmProvider, Provider, ProviderConfig, ProviderRegistry, send_via_stream, }; diff --git a/crates/owlen-core/src/mcp/remote_client.rs b/crates/owlen-core/src/mcp/remote_client.rs index 7cdf5e9..5a6e5bd 100644 --- a/crates/owlen-core/src/mcp/remote_client.rs +++ b/crates/owlen-core/src/mcp/remote_client.rs @@ -7,7 +7,10 @@ use crate::consent::{ConsentManager, ConsentScope}; use crate::tools::{Tool, WebScrapeTool, WebSearchTool}; use crate::types::ModelInfo; use crate::types::{ChatResponse, Message, Role}; -use crate::{Error, LlmProvider, Result, mode::Mode, send_via_stream}; +use crate::{ + ChatStream, Error, LlmProvider, Result, facade::llm_client::LlmClient, mode::Mode, + send_via_stream, +}; use anyhow::anyhow; use futures::{StreamExt, future::BoxFuture, stream}; use reqwest::Client as HttpClient; @@ -564,3 +567,27 @@ impl LlmProvider for RemoteMcpClient { }) } } + +#[async_trait::async_trait] +impl LlmClient for RemoteMcpClient { + async fn list_models(&self) -> Result> { + ::list_models(self).await + } + + async fn send_chat(&self, request: crate::types::ChatRequest) -> Result { + ::send_prompt(self, request).await + } + + async fn stream_chat(&self, request: crate::types::ChatRequest) -> Result { + let stream = ::stream_prompt(self, request).await?; + Ok(Box::pin(stream)) + } + + async fn list_tools(&self) -> Result> { + ::list_tools(self).await + } + + async fn call_tool(&self, call: McpToolCall) -> Result { + ::call_tool(self, call).await + } +} diff --git a/crates/owlen-tui/src/chat_app.rs b/crates/owlen-tui/src/chat_app.rs index f72e3b5..67958ee 100644 --- a/crates/owlen-tui/src/chat_app.rs +++ b/crates/owlen-tui/src/chat_app.rs @@ -2,6 +2,7 @@ use anyhow::{Context, Result, anyhow}; use async_trait::async_trait; use chrono::{DateTime, Local, Utc}; use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; +use owlen_core::facade::llm_client::LlmClient; use owlen_core::mcp::remote_client::RemoteMcpClient; use owlen_core::mcp::{McpToolDescriptor, McpToolResponse}; use owlen_core::provider::{ @@ -9,7 +10,7 @@ use owlen_core::provider::{ ProviderType, }; use owlen_core::{ - Provider, ProviderConfig, + ProviderConfig, config::McpResourceConfig, model::DetailedModelInfo, oauth::{DeviceAuthorization, DevicePollState}, @@ -7581,22 +7582,25 @@ impl ChatApp { }; match client_result { - Ok(client) => match client.list_models().await { - Ok(mut provider_models) => { - for model in &mut provider_models { - model.provider = canonical_name.clone(); + Ok(client) => { + let client: Arc = Arc::new(client); + match client.list_models().await { + Ok(mut provider_models) => { + for model in &mut provider_models { + model.provider = canonical_name.clone(); + } + let statuses = Self::extract_scope_status(&provider_models); + Self::accumulate_scope_errors(&mut errors, &canonical_name, &statuses); + scope_status_map.insert(canonical_name.clone(), statuses); + models.extend(provider_models); + } + Err(err) => { + scope_status_map + .insert(canonical_name.clone(), ProviderScopeStatus::default()); + errors.push(format!("{}: {}", name, err)) } - let statuses = Self::extract_scope_status(&provider_models); - Self::accumulate_scope_errors(&mut errors, &canonical_name, &statuses); - scope_status_map.insert(canonical_name.clone(), statuses); - models.extend(provider_models); } - Err(err) => { - scope_status_map - .insert(canonical_name.clone(), ProviderScopeStatus::default()); - errors.push(format!("{}: {}", name, err)) - } - }, + } Err(err) => { scope_status_map.insert(canonical_name.clone(), ProviderScopeStatus::default()); errors.push(format!("{}: {}", canonical_name, err));