docs: add provider onboarding guide and update documentation for ProviderManager, health worker, and multi‑provider architecture
This commit is contained in:
62
docs/adding-providers.md
Normal file
62
docs/adding-providers.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Adding a Provider to Owlen
|
||||
|
||||
This guide complements `docs/provider-implementation.md` with a practical checklist for wiring a new model backend into the Phase 10 architecture.
|
||||
|
||||
## 1. Define the Provider Type
|
||||
|
||||
Providers live in their own crate (for example `owlen-providers`). Create a module that implements the `owlen_core::provider::ModelProvider` trait:
|
||||
|
||||
```rust
|
||||
pub struct MyProvider {
|
||||
client: MyHttpClient,
|
||||
metadata: ProviderMetadata,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ModelProvider for MyProvider {
|
||||
fn metadata(&self) -> &ProviderMetadata { &self.metadata }
|
||||
async fn health_check(&self) -> Result<ProviderStatus> { ... }
|
||||
async fn list_models(&self) -> Result<Vec<ModelInfo>> { ... }
|
||||
async fn generate_stream(&self, request: GenerateRequest) -> Result<GenerateStream> { ... }
|
||||
}
|
||||
```
|
||||
|
||||
Set `ProviderMetadata::provider_type` to `ProviderType::Local` or `ProviderType::Cloud` so the TUI can label it correctly.
|
||||
|
||||
## 2. Register with `ProviderManager`
|
||||
|
||||
`ProviderManager` owns provider instances and tracks their health. In your startup code (usually `owlen-cli` or an MCP server), construct the provider and register it:
|
||||
|
||||
```rust
|
||||
let manager = ProviderManager::new(config);
|
||||
manager.register_provider(Arc::new(MyProvider::new(config)?.into())).await;
|
||||
```
|
||||
|
||||
The manager caches `ProviderStatus` values so the TUI can surface availability in the picker and background worker events.
|
||||
|
||||
## 3. Expose Through MCP (Optional)
|
||||
|
||||
For providers that should run out-of-process, implement an MCP server (`owlen-mcp-llm-server` demonstrates the pattern). The TUI uses `RemoteMcpClient`, so exposing `generate_text` keeps the UI completely decoupled from provider details.
|
||||
|
||||
## 4. Add Tests
|
||||
|
||||
Commit 13 introduced integration tests in `crates/owlen-providers/tests`. Follow this pattern to exercise:
|
||||
|
||||
- registration with `ProviderManager`
|
||||
- model aggregation across providers
|
||||
- routing of `generate` requests
|
||||
- provider status transitions when generation succeeds or fails
|
||||
|
||||
In-memory mocks are enough; the goal is to protect the trait contract and the manager’s health cache.
|
||||
|
||||
## 5. Document Configuration
|
||||
|
||||
Update `docs/configuration.md` and the default `config.toml` snippet so users can enable the new provider. Include environment variables, auth requirements, or special flags.
|
||||
|
||||
## 6. Update User-Facing Docs
|
||||
|
||||
- Add a short entry to the feature list in `README.md`.
|
||||
- Mention the new provider in `CHANGELOG.md` under the “Added” section.
|
||||
- If the provider requires troubleshooting steps, append them to `docs/troubleshooting.md`.
|
||||
|
||||
Following these steps keeps the provider lifecycle consistent with Owlen’s multi-provider architecture: providers register once, the manager handles orchestration, and the TUI reacts via message-driven updates.
|
||||
@@ -6,7 +6,8 @@ This document provides a high-level overview of the Owlen architecture. Its purp
|
||||
|
||||
The architecture is designed to be modular and extensible, centered around a few key concepts:
|
||||
|
||||
- **Providers**: Connect to various LLM APIs (Ollama, OpenAI, etc.).
|
||||
- **Provider Manager**: Coordinates multiple `ModelProvider` implementations, aggregates model metadata, and caches health status for the UI.
|
||||
- **Providers**: Concrete backends (Ollama Local, Ollama Cloud, future providers) accessed either directly or through MCP servers.
|
||||
- **Session**: Manages the conversation history and state.
|
||||
- **TUI**: The terminal user interface, built with `ratatui`.
|
||||
- **Events**: A system for handling user input and other events.
|
||||
@@ -16,18 +17,20 @@ The architecture is designed to be modular and extensible, centered around a few
|
||||
A simplified diagram of how components interact:
|
||||
|
||||
```
|
||||
[User Input] -> [Event Loop] -> [Session Controller] -> [Provider]
|
||||
^ |
|
||||
| v
|
||||
[TUI Renderer] <------------------------------------ [API Response]
|
||||
[User Input] -> [Event Loop] -> [Message Handler] -> [Session Controller] -> [Provider Manager] -> [Provider]
|
||||
^ |
|
||||
| v
|
||||
[TUI Renderer] <- [AppMessage Stream] <- [Background Worker] <--------------- [Provider Health]
|
||||
```
|
||||
|
||||
1. **User Input**: The user interacts with the TUI, generating events (e.g., key presses).
|
||||
2. **Event Loop**: The main event loop in `owlen-tui` captures these events.
|
||||
3. **Session Controller**: The event is processed, and if it's a prompt, the session controller sends a request to the current provider.
|
||||
4. **Provider**: The provider formats the request for the specific LLM API and sends it.
|
||||
5. **API Response**: The LLM API returns a response.
|
||||
6. **TUI Renderer**: The response is processed, the session state is updated, and the TUI is re-rendered to display the new information.
|
||||
2. **Event Loop**: The non-blocking event loop in `owlen-tui` bundles raw input, async session events, and background health updates into `AppMessage` events.
|
||||
3. **Message Handler**: `App::handle_message` centralises dispatch, updating runtime state (chat, model picker, provider indicators) before the UI redraw.
|
||||
4. **Session Controller**: Prompt events create `GenerateRequest`s that flow through `ProviderManager::generate` to the designated provider.
|
||||
5. **Provider**: The provider formats requests for its API and streams back `GenerateChunk`s.
|
||||
6. **Provider Manager**: Tracks health while streaming; errors mark a provider unavailable so background workers and the model picker reflect the state.
|
||||
7. **Background Worker**: A periodic task runs health checks and emits status updates as `AppMessage::ProviderStatus` events.
|
||||
8. **TUI Renderer**: The response is processed, the session state is updated, and the TUI is re-rendered to display the new information.
|
||||
|
||||
## Crate Breakdown
|
||||
|
||||
@@ -106,7 +109,7 @@ The session management system is responsible for tracking the state of a convers
|
||||
- **`SessionController`**: This is the high-level controller that manages the active conversation. It handles:
|
||||
- Storing and retrieving conversation history via the `ConversationManager`.
|
||||
- Managing the context that is sent to the LLM provider.
|
||||
- Switching between different models.
|
||||
- Switching between different models by selecting a provider ID managed by `ProviderManager`.
|
||||
- Sending requests to the provider and handling the responses (both streaming and complete).
|
||||
|
||||
When a user sends a message, the `SessionController` adds the message to the current `Conversation`, sends the updated message list to the `Provider`, and then adds the provider's response to the `Conversation`.
|
||||
|
||||
@@ -2,24 +2,22 @@
|
||||
|
||||
This guide explains how to implement a new provider for Owlen. Providers are the components that connect to different LLM APIs.
|
||||
|
||||
## The `Provider` Trait
|
||||
## The `ModelProvider` Trait
|
||||
|
||||
The core of the provider system is the `Provider` trait, located in `owlen-core`. Any new provider must implement this trait.
|
||||
The core of the provider system is the `ModelProvider` trait, located in `owlen-core::provider`. Any new provider must implement this async trait so it can be managed by `ProviderManager`.
|
||||
|
||||
Here is a simplified version of the trait:
|
||||
|
||||
```rust
|
||||
use async_trait::async_trait;
|
||||
use owlen_core::model::Model;
|
||||
use owlen_core::session::Session;
|
||||
use owlen_core::provider::{GenerateChunk, GenerateRequest, GenerateStream, ModelInfo, ProviderMetadata, ProviderStatus};
|
||||
|
||||
#[async_trait]
|
||||
pub trait Provider {
|
||||
/// Returns the name of the provider.
|
||||
fn name(&self) -> &str;
|
||||
|
||||
/// Sends the session to the provider and returns the response.
|
||||
async fn chat(&self, session: &Session, model: &Model) -> Result<String, anyhow::Error>;
|
||||
pub trait ModelProvider: Send + Sync {
|
||||
fn metadata(&self) -> &ProviderMetadata;
|
||||
async fn health_check(&self) -> owlen_core::Result<ProviderStatus>;
|
||||
async fn list_models(&self) -> owlen_core::Result<Vec<ModelInfo>>;
|
||||
async fn generate_stream(&self, request: GenerateRequest) -> owlen_core::Result<GenerateStream>;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -35,41 +33,66 @@ In your new crate's `lib.rs`, you will define a struct for your provider and imp
|
||||
|
||||
```rust
|
||||
use async_trait::async_trait;
|
||||
use owlen_core::model::Model;
|
||||
use owlen_core::Provider;
|
||||
use owlen_core::session::Session;
|
||||
use owlen_core::provider::{
|
||||
GenerateRequest, GenerateStream, ModelInfo, ModelProvider, ProviderMetadata,
|
||||
ProviderStatus, ProviderType,
|
||||
};
|
||||
|
||||
pub struct MyProvider;
|
||||
pub struct MyProvider {
|
||||
metadata: ProviderMetadata,
|
||||
client: MyHttpClient,
|
||||
}
|
||||
|
||||
impl MyProvider {
|
||||
pub fn new(config: &MyConfig) -> owlen_core::Result<Self> {
|
||||
let metadata = ProviderMetadata::new(
|
||||
"my_provider",
|
||||
"My Provider",
|
||||
ProviderType::Cloud,
|
||||
true,
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
metadata,
|
||||
client: MyHttpClient::new(config)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Provider for MyProvider {
|
||||
fn name(&self) -> &str {
|
||||
"my-provider"
|
||||
impl ModelProvider for MyProvider {
|
||||
fn metadata(&self) -> &ProviderMetadata {
|
||||
&self.metadata
|
||||
}
|
||||
|
||||
async fn chat(&self, session: &Session, model: &Model) -> Result<String, anyhow::Error> {
|
||||
// 1. Get the conversation history from the session.
|
||||
let history = session.get_messages();
|
||||
async fn health_check(&self) -> owlen_core::Result<ProviderStatus> {
|
||||
self.client.ping().await.map(|_| ProviderStatus::Available)
|
||||
}
|
||||
|
||||
// 2. Format the request for your provider's API.
|
||||
// This might involve creating a JSON body with the messages.
|
||||
async fn list_models(&self) -> owlen_core::Result<Vec<ModelInfo>> {
|
||||
self.client.list_models().await
|
||||
}
|
||||
|
||||
// 3. Send the request to the API using a client like reqwest.
|
||||
|
||||
// 4. Parse the response from the API.
|
||||
|
||||
// 5. Return the content of the response as a String.
|
||||
|
||||
Ok("Hello from my provider!".to_string())
|
||||
async fn generate_stream(&self, request: GenerateRequest) -> owlen_core::Result<GenerateStream> {
|
||||
self.client.generate(request).await
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Integrating with Owlen
|
||||
|
||||
Once your provider is implemented, you will need to integrate it into the main Owlen application.
|
||||
Once your provider is implemented, you will need to register it with the `ProviderManager` and surface it to users.
|
||||
|
||||
1. **Add your provider crate** as a dependency to `owlen-cli`.
|
||||
2. **In `owlen-cli`, modify the provider registration** to include your new provider. This will likely involve adding it to a list of available providers that the user can select from in the configuration.
|
||||
1. **Add your provider crate** as a dependency to the component that will host it (an MCP server or `owlen-cli`).
|
||||
2. **Register the provider** with `ProviderManager` during startup:
|
||||
|
||||
This guide provides a basic outline. For more detailed examples, you can look at the existing provider implementations, such as `owlen-ollama`.
|
||||
```rust
|
||||
let manager = ProviderManager::new(config);
|
||||
manager.register_provider(Arc::new(MyProvider::new(config)?)).await;
|
||||
```
|
||||
|
||||
3. **Update configuration docs/examples** so the provider has a `[providers.my_provider]` entry.
|
||||
4. **Expose via MCP (optional)** if the provider should run out-of-process. Owlen’s TUI talks to providers exclusively via MCP after Phase 10.
|
||||
5. **Add tests** similar to `crates/owlen-providers/tests/integration_test.rs` that exercise registration, model aggregation, generation routing, and health transitions.
|
||||
|
||||
For concrete examples, see the Ollama providers in `crates/owlen-providers/` and the integration tests added in commit 13.
|
||||
|
||||
@@ -31,6 +31,7 @@ Owlen now queries both the local daemon and Ollama Cloud and shows them side-by-
|
||||
4. **Keep the base URL local.** The cloud setup command no longer overrides `providers.ollama.base_url` unless `--force-cloud-base-url` is passed. If you changed it manually, edit `config.toml` or run `owlen config doctor` to restore the default `http://localhost:11434` value.
|
||||
|
||||
Once the daemon responds again, the picker will automatically merge the updated local list with the cloud catalogue.
|
||||
Owlen runs a background health worker every 30 seconds; once the daemon responds it will update the picker automatically without needing a restart.
|
||||
|
||||
## Terminal Compatibility Issues
|
||||
|
||||
|
||||
Reference in New Issue
Block a user