956 lines
31 KiB
Markdown
956 lines
31 KiB
Markdown
|
|
# BotCoder Hybrid Architecture v2.0
|
||
|
|
## CLI + Optional Multi-Agent Facade (BYOK vs BotServer)
|
||
|
|
|
||
|
|
## Executive Summary
|
||
|
|
|
||
|
|
**BotCoder** exists as a **terminal-based AI coding agent** with real-time streaming and tool execution. This document outlines how to extend it into a **hybrid CLI/multi-agent OS** that can:
|
||
|
|
|
||
|
|
1. **Work standalone (BYOK)** - Direct LLM access, local execution
|
||
|
|
2. **Use botserver facade** - Leverage Mantis Farm agents when available
|
||
|
|
3. **Switch dynamically** - Fall back to local if botserver unavailable
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Current BotCoder Architecture
|
||
|
|
|
||
|
|
### Existing CLI Implementation (`/home/rodriguez/src/pgm/botcoder`)
|
||
|
|
|
||
|
|
**Dependencies:**
|
||
|
|
```toml
|
||
|
|
tokio = "1.42" # Async runtime
|
||
|
|
reqwest = "0.12" # HTTP client
|
||
|
|
ratatui = "0.29" # TUI framework
|
||
|
|
crossterm = "0.29" # Terminal handling
|
||
|
|
futures = "0.3" # Async utilities
|
||
|
|
regex = "1.10" # Pattern matching
|
||
|
|
```
|
||
|
|
|
||
|
|
**Core Features:**
|
||
|
|
- ✅ Real-time streaming LLM responses
|
||
|
|
- ✅ Tool execution (read_file, execute_command, write_file)
|
||
|
|
- ✅ Delta format parsing (git-style diffs)
|
||
|
|
- ✅ TPM rate limiting
|
||
|
|
- ✅ Conversation history management
|
||
|
|
- ✅ Animated TUI with ratatui
|
||
|
|
|
||
|
|
**Tool Support:**
|
||
|
|
```rust
|
||
|
|
// Currently supported tools
|
||
|
|
fn execute_tool(tool: &str, param: &str, project_root: &str) -> String {
|
||
|
|
match tool {
|
||
|
|
"read_file" => read_file(param),
|
||
|
|
"execute_command" => execute_command(param, project_root),
|
||
|
|
"write_file" => write_file(param),
|
||
|
|
"list_files" => list_files(param, project_root),
|
||
|
|
_ => format!("Unknown tool: {}", tool),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**LLM Integration:**
|
||
|
|
```rust
|
||
|
|
// Direct Azure OpenAI client
|
||
|
|
mod llm {
|
||
|
|
pub struct AzureOpenAIClient {
|
||
|
|
endpoint: String,
|
||
|
|
api_key: String,
|
||
|
|
deployment: String,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl LLMProvider for AzureOpenAIClient {
|
||
|
|
async fn generate(&self, prompt: &str, params: &serde_json::Value)
|
||
|
|
-> Result<String, Box<dyn std::error::Error>>;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Proposed Hybrid Architecture
|
||
|
|
|
||
|
|
```
|
||
|
|
┌─────────────────────────────────────────────────────────────────┐
|
||
|
|
│ BOTCODER HYBRID MODE │
|
||
|
|
├─────────────────────────────────────────────────────────────────┤
|
||
|
|
│ │
|
||
|
|
│ ┌──────────────────────────────────────────────────────────┐ │
|
||
|
|
│ │ BOTCODER CLI (main.rs) │ │
|
||
|
|
│ │ - TUI interface (ratatui) │ │
|
||
|
|
│ │ - Tool execution │ │
|
||
|
|
│ │ - Delta parsing │ │
|
||
|
|
│ │ - Rate limiting │ │
|
||
|
|
│ └──────────────────────────────────────────────────────────┘ │
|
||
|
|
│ │ │
|
||
|
|
│ ▼ │
|
||
|
|
│ ┌──────────────────────────────────────────────────────────┐ │
|
||
|
|
│ │ LLM PROVIDER TRAIT (abstraction) │ │
|
||
|
|
│ └──────────────────────────────────────────────────────────┘ │
|
||
|
|
│ │ │
|
||
|
|
│ ┌───────────────┴───────────────┐ │
|
||
|
|
│ ▼ ▼ │
|
||
|
|
│ ┌─────────────────────┐ ┌─────────────────────┐ │
|
||
|
|
│ │ DIRECT LLM │ │ BOTSERVER FACADE │ │
|
||
|
|
│ │ (BYOK Mode) │ │ (Multi-Agent Mode) │ │
|
||
|
|
│ │ │ │ │ │
|
||
|
|
│ │ - Azure OpenAI │ │ - Mantis #1-4 │ │
|
||
|
|
│ │ - Anthropic │ │ - Mantis #5-12 │ │
|
||
|
|
│ │ - OpenAI │ │ - Orchestrator │ │
|
||
|
|
│ │ - Local LLM │ │ - WebSocket │ │
|
||
|
|
│ └─────────────────────┘ └─────────────────────┘ │
|
||
|
|
│ │ │ │
|
||
|
|
│ │ (Optional) │
|
||
|
|
│ ▼ ▼ │
|
||
|
|
│ ┌─────────────────────┐ ┌─────────────────────┐ │
|
||
|
|
│ │ LOCAL EXECUTION │ │ AGENT EXECUTION │ │
|
||
|
|
│ │ │ │ │ │
|
||
|
|
│ │ - File operations │ │ - Containerized │ │
|
||
|
|
│ │ - Command execution │ │ - AgentExecutor │ │
|
||
|
|
│ │ - Git operations │ │ - Browser automation│ │
|
||
|
|
│ │ - Docker control │ │ - Test generation │ │
|
||
|
|
│ └─────────────────────┘ └─────────────────────┘ │
|
||
|
|
│ │
|
||
|
|
└─────────────────────────────────────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Implementation Plan
|
||
|
|
|
||
|
|
### Phase 1: LLM Provider Abstraction (Week 1)
|
||
|
|
|
||
|
|
**Goal:** Create trait-based system for multiple LLM backends
|
||
|
|
|
||
|
|
**File:** `src/llm/mod.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use async_trait::async_trait;
|
||
|
|
|
||
|
|
/// Unified LLM provider trait
|
||
|
|
#[async_trait]
|
||
|
|
pub trait LLMProvider: Send + Sync {
|
||
|
|
/// Generate completion with streaming support
|
||
|
|
async fn generate_stream(
|
||
|
|
&self,
|
||
|
|
prompt: &str,
|
||
|
|
params: &GenerationParams,
|
||
|
|
) -> Result<StreamResponse, LLMError>;
|
||
|
|
|
||
|
|
/// Generate completion (non-streaming)
|
||
|
|
async fn generate(
|
||
|
|
&self,
|
||
|
|
prompt: &str,
|
||
|
|
params: &GenerationParams,
|
||
|
|
) -> Result<String, LLMError>;
|
||
|
|
|
||
|
|
/// Get provider capabilities
|
||
|
|
fn capabilities(&self) -> ProviderCapabilities;
|
||
|
|
|
||
|
|
/// Get provider name
|
||
|
|
fn name(&self) -> &str;
|
||
|
|
}
|
||
|
|
|
||
|
|
pub struct GenerationParams {
|
||
|
|
pub temperature: f32,
|
||
|
|
pub max_tokens: u32,
|
||
|
|
pub top_p: f32,
|
||
|
|
pub tools: Vec<ToolDefinition>,
|
||
|
|
pub system_prompt: Option<String>,
|
||
|
|
}
|
||
|
|
|
||
|
|
pub struct StreamResponse {
|
||
|
|
pub content_stream: tokio_stream::wrappers::ReceiverStream<String>,
|
||
|
|
pub tool_calls: Vec<ToolCall>,
|
||
|
|
pub usage: TokenUsage,
|
||
|
|
}
|
||
|
|
|
||
|
|
pub struct ProviderCapabilities {
|
||
|
|
pub streaming: bool,
|
||
|
|
pub tools: bool,
|
||
|
|
pub max_tokens: u32,
|
||
|
|
pub supports_vision: bool,
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Implementations:**
|
||
|
|
|
||
|
|
```rust
|
||
|
|
// src/llm/azure_openai.rs
|
||
|
|
pub struct AzureOpenAIClient {
|
||
|
|
endpoint: String,
|
||
|
|
api_key: String,
|
||
|
|
deployment: String,
|
||
|
|
client: reqwest::Client,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[async_trait]
|
||
|
|
impl LLMProvider for AzureOpenAIClient {
|
||
|
|
async fn generate(&self, prompt: &str, params: &GenerationParams)
|
||
|
|
-> Result<String, LLMError> {
|
||
|
|
// Existing implementation
|
||
|
|
}
|
||
|
|
|
||
|
|
fn capabilities(&self) -> ProviderCapabilities {
|
||
|
|
ProviderCapabilities {
|
||
|
|
streaming: true,
|
||
|
|
tools: true,
|
||
|
|
max_tokens: 4096,
|
||
|
|
supports_vision: false,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn name(&self) -> &str {
|
||
|
|
"azure-openai"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// src/llm/anthropic.rs
|
||
|
|
pub struct AnthropicClient {
|
||
|
|
api_key: String,
|
||
|
|
client: reqwest::Client,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[async_trait]
|
||
|
|
impl LLMProvider for AnthropicClient {
|
||
|
|
async fn generate(&self, prompt: &str, params: &GenerationParams)
|
||
|
|
-> Result<String, LLMError> {
|
||
|
|
// Anthropic API implementation
|
||
|
|
}
|
||
|
|
|
||
|
|
fn capabilities(&self) -> ProviderCapabilities {
|
||
|
|
ProviderCapabilities {
|
||
|
|
streaming: true,
|
||
|
|
tools: true,
|
||
|
|
max_tokens: 8192,
|
||
|
|
supports_vision: true,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn name(&self) -> &str {
|
||
|
|
"anthropic"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// src/llm/botserver_facade.rs
|
||
|
|
pub struct BotServerFacade {
|
||
|
|
base_url: String,
|
||
|
|
api_key: Option<String>,
|
||
|
|
client: reqwest::Client,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[async_trait]
|
||
|
|
impl LLMProvider for BotServerFacade {
|
||
|
|
async fn generate(&self, prompt: &str, params: &GenerationParams)
|
||
|
|
-> Result<String, LLMError> {
|
||
|
|
// Instead of direct LLM call, use botserver's orchestrator
|
||
|
|
// 1. Classify intent
|
||
|
|
// 2. Execute multi-agent pipeline
|
||
|
|
// 3. Return aggregated result
|
||
|
|
}
|
||
|
|
|
||
|
|
fn capabilities(&self) -> ProviderCapabilities {
|
||
|
|
ProviderCapabilities {
|
||
|
|
streaming: true, // Via WebSocket
|
||
|
|
tools: true, // Via AgentExecutor
|
||
|
|
max_tokens: 128000, // Multi-agent consensus
|
||
|
|
supports_vision: true, // Via Browser Agent
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn name(&self) -> &str {
|
||
|
|
"botserver-mantis-farm"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Configuration:**
|
||
|
|
|
||
|
|
```rust
|
||
|
|
// src/config.rs
|
||
|
|
#[derive(Debug, Clone)]
|
||
|
|
pub struct BotCoderConfig {
|
||
|
|
pub llm_provider: LLMProviderType,
|
||
|
|
pub botserver_url: Option<String>,
|
||
|
|
pub project_path: PathBuf,
|
||
|
|
pub enable_facade: bool,
|
||
|
|
pub fallback_to_local: bool,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Clone)]
|
||
|
|
pub enum LLMProviderType {
|
||
|
|
AzureOpenAI,
|
||
|
|
Anthropic,
|
||
|
|
OpenAI,
|
||
|
|
LocalLLM,
|
||
|
|
BotServerFacade, // Use Mantis Farm
|
||
|
|
}
|
||
|
|
|
||
|
|
impl BotCoderConfig {
|
||
|
|
pub fn from_env() -> Result<Self, ConfigError> {
|
||
|
|
let llm_provider = match env::var("LLM_PROVIDER").as_deref() {
|
||
|
|
Ok("azure") => LLMProviderType::AzureOpenAI,
|
||
|
|
Ok("anthropic") => LLMProviderType::Anthropic,
|
||
|
|
Ok("botserver") => LLMProviderType::BotServerFacade,
|
||
|
|
_ => LLMProviderType::AzureOpenAI, // Default
|
||
|
|
};
|
||
|
|
|
||
|
|
let botserver_url = env::var("BOTSERVER_URL").ok();
|
||
|
|
let enable_facade = env::var("ENABLE_BOTSERVER_FACADE")
|
||
|
|
.unwrap_or_else(|_| "false".to_string()) == "true";
|
||
|
|
|
||
|
|
Ok(Self {
|
||
|
|
llm_provider,
|
||
|
|
botserver_url,
|
||
|
|
project_path: env::var("PROJECT_PATH")?.into(),
|
||
|
|
enable_facade,
|
||
|
|
fallback_to_local: true,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Phase 2: Multi-Agent Facade Integration (Week 2)
|
||
|
|
|
||
|
|
**Goal:** Connect to botserver's Mantis Farm when available
|
||
|
|
|
||
|
|
**File:** `src/botserver_client.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use reqwest::Client;
|
||
|
|
use serde::{Deserialize, Serialize};
|
||
|
|
|
||
|
|
pub struct BotServerClient {
|
||
|
|
base_url: String,
|
||
|
|
api_key: Option<String>,
|
||
|
|
client: Client,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl BotServerClient {
|
||
|
|
pub fn new(base_url: String, api_key: Option<String>) -> Self {
|
||
|
|
Self {
|
||
|
|
base_url,
|
||
|
|
api_key,
|
||
|
|
client: Client::new(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Classify intent using botserver's intent classifier
|
||
|
|
pub async fn classify_intent(&self, text: &str)
|
||
|
|
-> Result<ClassifiedIntent, BotServerError> {
|
||
|
|
let url = format!("{}/api/autotask/classify", self.base_url);
|
||
|
|
|
||
|
|
let response = self.client
|
||
|
|
.post(&url)
|
||
|
|
.json(&serde_json::json!({ "text": text }))
|
||
|
|
.header("Authorization", self.api_key.as_ref().map(|k| format!("Bearer {}", k)).unwrap_or_default())
|
||
|
|
.send()
|
||
|
|
.await?;
|
||
|
|
|
||
|
|
if response.status().is_success() {
|
||
|
|
Ok(response.json().await?)
|
||
|
|
} else {
|
||
|
|
Err(BotServerError::ClassificationFailed(response.text().await?))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Execute multi-agent pipeline
|
||
|
|
pub async fn execute_pipeline(&self, classification: &ClassifiedIntent)
|
||
|
|
-> Result<OrchestrationResult, BotServerError> {
|
||
|
|
let url = format!("{}/api/autotask/execute", self.base_url);
|
||
|
|
|
||
|
|
let response = self.client
|
||
|
|
.post(&url)
|
||
|
|
.json(classification)
|
||
|
|
.header("Authorization", self.api_key.as_ref().map(|k| format!("Bearer {}", k)).unwrap_or_default())
|
||
|
|
.send()
|
||
|
|
.await?;
|
||
|
|
|
||
|
|
if response.status().is_success() {
|
||
|
|
Ok(response.json().await?)
|
||
|
|
} else {
|
||
|
|
Err(BotServerError::PipelineFailed(response.text().await?))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Subscribe to WebSocket progress updates
|
||
|
|
pub async fn subscribe_progress(&self, task_id: &str)
|
||
|
|
-> Result<tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>, BotServerError> {
|
||
|
|
let ws_url = format!(
|
||
|
|
"{}/ws/task-progress/{}",
|
||
|
|
self.base_url.replace("http", "ws"),
|
||
|
|
task_id
|
||
|
|
);
|
||
|
|
|
||
|
|
tokio_tungstenite::connect_async(&ws_url).await
|
||
|
|
.map_err(BotServerError::WebSocketError)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Use specialized agents directly
|
||
|
|
pub async fn query_agent(&self, agent_id: u8, query: &str)
|
||
|
|
-> Result<AgentResponse, BotServerError> {
|
||
|
|
match agent_id {
|
||
|
|
5 => self.query_editor_agent(query).await,
|
||
|
|
6 => self.query_database_agent(query).await,
|
||
|
|
7 => self.query_git_agent(query).await,
|
||
|
|
8 => self.query_test_agent(query).await,
|
||
|
|
9 => self.query_browser_agent(query).await,
|
||
|
|
10 => self.query_terminal_agent(query).await,
|
||
|
|
11 => self.query_docs_agent(query).await,
|
||
|
|
12 => self.query_security_agent(query).await,
|
||
|
|
_ => Err(BotServerError::InvalidAgent(agent_id)),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Specific agent methods
|
||
|
|
async fn query_editor_agent(&self, query: &str)
|
||
|
|
-> Result<AgentResponse, BotServerError> {
|
||
|
|
// POST /api/botcoder/editor/query
|
||
|
|
let url = format!("{}/api/botcoder/editor/query", self.base_url);
|
||
|
|
|
||
|
|
let response = self.client
|
||
|
|
.post(&url)
|
||
|
|
.json(&serde_json::json!({ "query": query }))
|
||
|
|
.send()
|
||
|
|
.await?;
|
||
|
|
|
||
|
|
Ok(response.json().await?)
|
||
|
|
}
|
||
|
|
|
||
|
|
async fn query_database_agent(&self, query: &str)
|
||
|
|
-> Result<AgentResponse, BotServerError> {
|
||
|
|
// Query database schema, optimize queries
|
||
|
|
let url = format!("{}/api/botcoder/database/query", self.base_url);
|
||
|
|
|
||
|
|
let response = self.client
|
||
|
|
.post(&url)
|
||
|
|
.json(&serde_json::json!({ "query": query }))
|
||
|
|
.send()
|
||
|
|
.await?;
|
||
|
|
|
||
|
|
Ok(response.json().await?)
|
||
|
|
}
|
||
|
|
|
||
|
|
// ... other agent methods
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Deserialize)]
|
||
|
|
pub struct ClassifiedIntent {
|
||
|
|
pub intent_type: String,
|
||
|
|
pub entities: IntentEntities,
|
||
|
|
pub original_text: String,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Deserialize)]
|
||
|
|
pub struct OrchestrationResult {
|
||
|
|
pub success: bool,
|
||
|
|
pub task_id: String,
|
||
|
|
pub stages_completed: u8,
|
||
|
|
pub app_url: Option<String>,
|
||
|
|
pub message: String,
|
||
|
|
pub created_resources: Vec<CreatedResource>,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, thiserror::Error)]
|
||
|
|
pub enum BotServerError {
|
||
|
|
#[error("Classification failed: {0}")]
|
||
|
|
ClassificationFailed(String),
|
||
|
|
|
||
|
|
#[error("Pipeline execution failed: {0}")]
|
||
|
|
PipelineFailed(String),
|
||
|
|
|
||
|
|
#[error("WebSocket error: {0}")]
|
||
|
|
WebSocketError(#[from] tokio_tungstenite::tungstenite::Error),
|
||
|
|
|
||
|
|
#[error("Invalid agent ID: {0}")]
|
||
|
|
InvalidAgent(u8),
|
||
|
|
|
||
|
|
#[error("HTTP error: {0}")]
|
||
|
|
HttpError(#[from] reqwest::Error),
|
||
|
|
|
||
|
|
#[error("JSON error: {0}")]
|
||
|
|
JsonError(#[from] serde_json::Error),
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Phase 3: Unified Tool Execution (Week 2-3)
|
||
|
|
|
||
|
|
**Goal:** Abstract tool execution to work locally or via agents
|
||
|
|
|
||
|
|
**File:** `src/tools/mod.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use async_trait::async_trait;
|
||
|
|
|
||
|
|
/// Unified tool execution trait
|
||
|
|
#[async_trait]
|
||
|
|
pub trait ToolExecutor: Send + Sync {
|
||
|
|
async fn execute(&self, tool: &ToolCall, context: &ExecutionContext)
|
||
|
|
-> Result<ToolResult, ToolError>;
|
||
|
|
|
||
|
|
fn supports_tool(&self, tool_name: &str) -> bool;
|
||
|
|
}
|
||
|
|
|
||
|
|
pub struct ToolCall {
|
||
|
|
pub name: String,
|
||
|
|
pub parameters: serde_json::Value,
|
||
|
|
pub agent_id: Option<u8>, // Which agent should execute
|
||
|
|
}
|
||
|
|
|
||
|
|
pub struct ToolResult {
|
||
|
|
pub output: String,
|
||
|
|
pub exit_code: i32,
|
||
|
|
pub metadata: serde_json::Value,
|
||
|
|
}
|
||
|
|
|
||
|
|
pub struct ExecutionContext {
|
||
|
|
pub project_path: PathBuf,
|
||
|
|
pub botserver_client: Option<BotServerClient>,
|
||
|
|
pub use_local_fallback: bool,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Local tool executor (existing implementation)
|
||
|
|
pub struct LocalToolExecutor {
|
||
|
|
project_root: PathBuf,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[async_trait]
|
||
|
|
impl ToolExecutor for LocalToolExecutor {
|
||
|
|
async fn execute(&self, tool: &ToolCall, context: &ExecutionContext)
|
||
|
|
-> Result<ToolResult, ToolError> {
|
||
|
|
match tool.name.as_str() {
|
||
|
|
"read_file" => self.read_file(tool.parameters).await,
|
||
|
|
"write_file" => self.write_file(tool.parameters).await,
|
||
|
|
"execute_command" => self.execute_command(tool.parameters).await,
|
||
|
|
"list_files" => self.list_files(tool.parameters).await,
|
||
|
|
"git_operation" => self.git_operation(tool.parameters).await,
|
||
|
|
_ => Err(ToolError::UnknownTool(tool.name.clone())),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn supports_tool(&self, tool_name: &str) -> bool {
|
||
|
|
matches!(tool_name,
|
||
|
|
"read_file" | "write_file" | "execute_command" |
|
||
|
|
"list_files" | "git_operation"
|
||
|
|
)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Agent-based tool executor (via botserver)
|
||
|
|
pub struct AgentToolExecutor {
|
||
|
|
botserver_client: BotServerClient,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[async_trait]
|
||
|
|
impl ToolExecutor for AgentToolExecutor {
|
||
|
|
async fn execute(&self, tool: &ToolCall, context: &ExecutionContext)
|
||
|
|
-> Result<ToolResult, ToolError> {
|
||
|
|
// Route to appropriate agent
|
||
|
|
let agent_id = tool.agent_id.unwrap_or_else(|| {
|
||
|
|
self.infer_agent_for_tool(&tool.name)
|
||
|
|
});
|
||
|
|
|
||
|
|
match self.botserver_client.query_agent(agent_id, &tool.parameters.to_string()).await {
|
||
|
|
Ok(response) => Ok(ToolResult {
|
||
|
|
output: response.output,
|
||
|
|
exit_code: response.exit_code,
|
||
|
|
metadata: response.metadata,
|
||
|
|
}),
|
||
|
|
Err(e) => {
|
||
|
|
// Fallback to local if enabled
|
||
|
|
if context.use_local_fallback {
|
||
|
|
warn!("Agent execution failed, falling back to local: {}", e);
|
||
|
|
LocalToolExecutor::new(context.project_path.clone()).execute(tool, context).await?
|
||
|
|
} else {
|
||
|
|
Err(ToolError::AgentError(e.to_string()))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn supports_tool(&self, tool_name: &str) -> bool {
|
||
|
|
matches!(tool_name,
|
||
|
|
"database_query" | "schema_visualize" | "git_commit" |
|
||
|
|
"test_generate" | "browser_record" | "docs_generate" |
|
||
|
|
"security_scan" | "code_refactor" | "optimize_query"
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn infer_agent_for_tool(&self, tool_name: &str) -> u8 {
|
||
|
|
match tool_name {
|
||
|
|
"code_refactor" | "syntax_check" => 5, // Editor Agent
|
||
|
|
"database_query" | "schema_visualize" | "optimize_query" => 6, // Database Agent
|
||
|
|
"git_commit" | "git_branch" | "git_merge" => 7, // Git Agent
|
||
|
|
"test_generate" | "coverage_report" => 8, // Test Agent
|
||
|
|
"browser_record" | "page_test" => 9, // Browser Agent
|
||
|
|
"shell_execute" | "docker_build" => 10, // Terminal Agent
|
||
|
|
"docs_generate" | "api_docs" => 11, // Docs Agent
|
||
|
|
"security_scan" | "vulnerability_check" => 12, // Security Agent
|
||
|
|
_ => 2, // Default to Builder Agent
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Phase 4: Hybrid Execution Loop (Week 3)
|
||
|
|
|
||
|
|
**Goal:** Main loop that seamlessly switches between local and agent execution
|
||
|
|
|
||
|
|
**File:** `src/main.rs` (modified)
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use llm::LLMProvider;
|
||
|
|
use tools::{LocalToolExecutor, AgentToolExecutor, ToolExecutor};
|
||
|
|
|
||
|
|
struct BotCoder {
|
||
|
|
config: BotCoderConfig,
|
||
|
|
llm_provider: Box<dyn LLMProvider>,
|
||
|
|
local_executor: LocalToolExecutor,
|
||
|
|
agent_executor: Option<AgentToolExecutor>,
|
||
|
|
botserver_client: Option<BotServerClient>,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl BotCoder {
|
||
|
|
pub async fn new(config: BotCoderConfig) -> Result<Self, Box<dyn std::error::Error>> {
|
||
|
|
// Initialize LLM provider based on config
|
||
|
|
let llm_provider: Box<dyn LLMProvider> = match config.llm_provider {
|
||
|
|
LLMProviderType::AzureOpenAI => {
|
||
|
|
Box::new(llm::AzureOpenAIClient::new()?)
|
||
|
|
}
|
||
|
|
LLMProviderType::Anthropic => {
|
||
|
|
Box::new(llm::AnthropicClient::new()?)
|
||
|
|
}
|
||
|
|
LLMProviderType::BotServerFacade => {
|
||
|
|
// Will use botserver client
|
||
|
|
Box::new(llm::BotServerFacade::new(
|
||
|
|
config.botserver_url.clone().unwrap()
|
||
|
|
)?)
|
||
|
|
}
|
||
|
|
_ => Box::new(llm::AzureOpenAIClient::new()?),
|
||
|
|
};
|
||
|
|
|
||
|
|
// Initialize tool executors
|
||
|
|
let local_executor = LocalToolExecutor::new(config.project_path.clone());
|
||
|
|
|
||
|
|
let mut agent_executor = None;
|
||
|
|
let mut botserver_client = None;
|
||
|
|
|
||
|
|
// Try to connect to botserver if enabled
|
||
|
|
if config.enable_facade {
|
||
|
|
if let Some(url) = &config.botserver_url {
|
||
|
|
match BotServerClient::new(url.clone(), None).health_check().await {
|
||
|
|
Ok(()) => {
|
||
|
|
println!("✓ Connected to botserver at {}", url);
|
||
|
|
let client = BotServerClient::new(url.clone(), None);
|
||
|
|
botserver_client = Some(client.clone());
|
||
|
|
agent_executor = Some(AgentToolExecutor::new(client));
|
||
|
|
}
|
||
|
|
Err(e) => {
|
||
|
|
warn!("Failed to connect to botserver: {}", e);
|
||
|
|
if config.fallback_to_local {
|
||
|
|
println!("⚠ Falling back to local execution");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Ok(Self {
|
||
|
|
config,
|
||
|
|
llm_provider,
|
||
|
|
local_executor,
|
||
|
|
agent_executor,
|
||
|
|
botserver_client,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
pub async fn run(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||
|
|
let mut iteration = 0;
|
||
|
|
let mut conversation_history: Vec<String> = Vec::new();
|
||
|
|
|
||
|
|
loop {
|
||
|
|
iteration += 1;
|
||
|
|
println!("=== ITERATION {} ===", iteration);
|
||
|
|
|
||
|
|
// Display execution mode
|
||
|
|
if self.agent_executor.is_some() {
|
||
|
|
println!("Mode: Multi-Agent (BotServer Facade)");
|
||
|
|
println!("Agents Available: Mantis #1-12");
|
||
|
|
} else {
|
||
|
|
println!("Mode: Local (BYOK)");
|
||
|
|
}
|
||
|
|
println!();
|
||
|
|
|
||
|
|
// Build context
|
||
|
|
let context = self.build_context(&conversation_history);
|
||
|
|
|
||
|
|
// Generate response (streaming)
|
||
|
|
let response = match self.llm_provider.generate_stream(&context, &Default::default()).await {
|
||
|
|
Ok(r) => r,
|
||
|
|
Err(e) => {
|
||
|
|
// Try fallback to local if botserver fails
|
||
|
|
if self.agent_executor.is_some() && self.config.fallback_to_local {
|
||
|
|
warn!("LLM provider failed, trying fallback: {}", e);
|
||
|
|
// Switch to local provider
|
||
|
|
continue;
|
||
|
|
} else {
|
||
|
|
return Err(e.into());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Stream response to TUI
|
||
|
|
self.display_streaming_response(response).await?;
|
||
|
|
|
||
|
|
// Extract tools from response
|
||
|
|
let tools = self.extract_tools(&full_response);
|
||
|
|
|
||
|
|
// Execute tools (local or agent-based)
|
||
|
|
for tool in tools {
|
||
|
|
let result = self.execute_tool_hybrid(tool).await?;
|
||
|
|
conversation_history.push(format!("Tool: {}\nResult: {}", tool.name, result));
|
||
|
|
}
|
||
|
|
|
||
|
|
conversation_history.push(format!("Assistant: {}", full_response));
|
||
|
|
|
||
|
|
// Trim history
|
||
|
|
if conversation_history.len() > 20 {
|
||
|
|
conversation_history.drain(0..10);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async fn execute_tool_hybrid(&self, tool: ToolCall) -> Result<ToolResult, ToolError> {
|
||
|
|
let context = ExecutionContext {
|
||
|
|
project_path: self.config.project_path.clone(),
|
||
|
|
botserver_client: self.botserver_client.clone(),
|
||
|
|
use_local_fallback: self.config.fallback_to_local,
|
||
|
|
};
|
||
|
|
|
||
|
|
// Try agent executor first if available
|
||
|
|
if let Some(agent_executor) = &self.agent_executor {
|
||
|
|
if agent_executor.supports_tool(&tool.name) {
|
||
|
|
println!("🤖 Executing via Mantis Agent: {}", tool.name);
|
||
|
|
return agent_executor.execute(&tool, &context).await;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Fall back to local executor
|
||
|
|
if self.local_executor.supports_tool(&tool.name) {
|
||
|
|
println!("🔧 Executing locally: {}", tool.name);
|
||
|
|
return self.local_executor.execute(&tool, &context).await;
|
||
|
|
}
|
||
|
|
|
||
|
|
Err(ToolError::UnknownTool(tool.name))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Usage Examples
|
||
|
|
|
||
|
|
### Example 1: Local Mode (BYOK)
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# .env configuration
|
||
|
|
LLM_PROVIDER=azure
|
||
|
|
PROJECT_PATH=/home/user/myproject
|
||
|
|
ENABLE_BOTSERVER_FACADE=false
|
||
|
|
```
|
||
|
|
|
||
|
|
```bash
|
||
|
|
$ botcoder
|
||
|
|
=== ITERATION 1 ===
|
||
|
|
Mode: Local (BYOK)
|
||
|
|
✓ Azure OpenAI connected
|
||
|
|
|
||
|
|
> Add authentication to this Rust project
|
||
|
|
|
||
|
|
[AI Reasoning...]
|
||
|
|
I'll add JWT authentication using the `jsonwebtoken` crate.
|
||
|
|
|
||
|
|
CHANGE: Cargo.toml
|
||
|
|
<<<<<<< CURRENT
|
||
|
|
[dependencies]
|
||
|
|
tokio = "1.0"
|
||
|
|
=======
|
||
|
|
[dependencies]
|
||
|
|
tokio = "1.0"
|
||
|
|
jsonwebtoken = "9.0"
|
||
|
|
=======
|
||
|
|
|
||
|
|
[EXECUTE] Tool 1/3: write_file -> Cargo.toml
|
||
|
|
✓ File updated
|
||
|
|
```
|
||
|
|
|
||
|
|
### Example 2: Multi-Agent Mode (BotServer Facade)
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# .env configuration
|
||
|
|
LLM_PROVIDER=botserver
|
||
|
|
BOTSERVER_URL=http://localhost:8080
|
||
|
|
PROJECT_PATH=/home/user/myproject
|
||
|
|
ENABLE_BOTSERVER_FACADE=true
|
||
|
|
FALLBACK_TO_LOCAL=true
|
||
|
|
```
|
||
|
|
|
||
|
|
```bash
|
||
|
|
$ botcoder
|
||
|
|
=== ITERATION 1 ===
|
||
|
|
✓ Connected to botserver at http://localhost:8080
|
||
|
|
Mode: Multi-Agent (BotServer Facade)
|
||
|
|
Agents Available: Mantis #1-12
|
||
|
|
|
||
|
|
> Create a CRM system with contacts and deals
|
||
|
|
|
||
|
|
[CLASSIFY] Intent: APP_CREATE
|
||
|
|
[PLAN] Mantis #1 breaking down request...
|
||
|
|
✓ 12 sub-tasks identified
|
||
|
|
✓ Estimated: 45 files, 98k tokens, 2.5 hours
|
||
|
|
|
||
|
|
[BUILD] Mantis #2 generating code...
|
||
|
|
✓ contacts table schema created
|
||
|
|
✓ deals table schema created
|
||
|
|
✓ Contact Manager page generated
|
||
|
|
✓ Deal Pipeline page generated
|
||
|
|
|
||
|
|
[REVIEW] Mantis #3 validating code...
|
||
|
|
✓ HTMX patterns verified
|
||
|
|
✓ Security checks passed
|
||
|
|
✓ 0 vulnerabilities found
|
||
|
|
|
||
|
|
[OPTIMIZE] Mantis #5 refactoring...
|
||
|
|
✓ Extracted duplicate code to utils.rs
|
||
|
|
✓ Added error handling wrappers
|
||
|
|
|
||
|
|
[TEST] Mantis #8 generating tests...
|
||
|
|
✓ 87% code coverage achieved
|
||
|
|
✓ E2E tests created (chromiumoxide)
|
||
|
|
|
||
|
|
[SECURITY] Mantis #12 scanning...
|
||
|
|
✓ 0 critical vulnerabilities
|
||
|
|
✓ All dependencies up to date
|
||
|
|
|
||
|
|
[DEPLOY] Mantis #4 deploying...
|
||
|
|
Target: Internal GB Platform
|
||
|
|
✓ App deployed to /apps/my-crm/
|
||
|
|
✓ Verify at http://localhost:8080/apps/my-crm/
|
||
|
|
|
||
|
|
[DOCUMENT] Mantis #11 generating docs...
|
||
|
|
✓ README.md created
|
||
|
|
✓ API documentation generated
|
||
|
|
|
||
|
|
✓ Pipeline complete in 1m 47s
|
||
|
|
```
|
||
|
|
|
||
|
|
### Example 3: Hybrid Mode (Automatic Fallback)
|
||
|
|
|
||
|
|
```bash
|
||
|
|
$ botcoder
|
||
|
|
=== ITERATION 1 ===
|
||
|
|
Mode: Multi-Agent (BotServer Facade)
|
||
|
|
✓ Connected to botserver
|
||
|
|
|
||
|
|
> Refactor this function for better performance
|
||
|
|
|
||
|
|
[EDITOR] Mantis #5 analyzing code...
|
||
|
|
⚠ BotServer connection lost
|
||
|
|
|
||
|
|
[FALLBACK] Switching to local mode...
|
||
|
|
[LOCAL] Analyzing with Azure OpenAI...
|
||
|
|
✓ Refactoring complete
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Benefits of Hybrid Architecture
|
||
|
|
|
||
|
|
### For Users (BYOK)
|
||
|
|
- ✅ **Privacy** - Code never leaves local machine
|
||
|
|
- ✅ **Speed** - Direct LLM access, no intermediate hops
|
||
|
|
- ✅ **Cost Control** - Use your own API keys
|
||
|
|
- ✅ **Offline Capable** - Works with local LLMs (llama.cpp, Ollama)
|
||
|
|
|
||
|
|
### For Users (BotServer Facade)
|
||
|
|
- ✅ **Multi-Agent Consensus** - 12 specialized agents collaborate
|
||
|
|
- ✅ **Advanced Capabilities** - Browser automation, security scanning, test generation
|
||
|
|
- ✅ **Visual Debugging** - Watch agent reasoning in Vibe Builder UI
|
||
|
|
- ✅ **Enterprise Features** - Team sharing, approval workflows, audit trails
|
||
|
|
|
||
|
|
### Seamless Switching
|
||
|
|
- ✅ **Automatic Fallback** - If botserver unavailable, use local
|
||
|
|
- ✅ **Tool Routing** - Use agent for complex tasks, local for simple ones
|
||
|
|
- ✅ **Cost Optimization** - Reserve expensive agents for hard problems
|
||
|
|
- ✅ **Progressive Enhancement** - Start local, upgrade to multi-agent as needed
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Configuration Matrix
|
||
|
|
|
||
|
|
| Scenario | LLM Provider | Tools | When to Use |
|
||
|
|
|----------|--------------|-------|-------------|
|
||
|
|
| **Local Development** | Azure/Anthropic (Direct) | Local file ops | Privacy-critical code |
|
||
|
|
| **Enterprise Project** | BotServer Facade | Agent-based | Complex refactoring |
|
||
|
|
| **Open Source** | Local LLM (Ollama) | Local | No API budget |
|
||
|
|
| **Learning** | BotServer Facade | Agent-based | Study agent reasoning |
|
||
|
|
| **CI/CD** | BotServer Facade | Agent-based | Automated testing |
|
||
|
|
| **Quick Fix** | Azure/Anthropic (Direct) | Local | Fast iteration |
|
||
|
|
| **Security Audit** | BotServer Facade | Mantis #12 | Comprehensive scan |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Implementation Roadmap
|
||
|
|
|
||
|
|
### Week 1: Foundation
|
||
|
|
- [x] Extract existing LLM client to trait
|
||
|
|
- [ ] Implement Azure OpenAI provider
|
||
|
|
- [ ] Implement Anthropic provider
|
||
|
|
- [ ] Add BotServerFacade provider (stub)
|
||
|
|
|
||
|
|
### Week 2: BotServer Integration
|
||
|
|
- [ ] Implement BotServerClient
|
||
|
|
- [ ] Add WebSocket progress streaming
|
||
|
|
- [ ] Implement agent query methods
|
||
|
|
- [ ] Add health check & fallback logic
|
||
|
|
|
||
|
|
### Week 3: Tool Execution
|
||
|
|
- [ ] Refactor existing tools to trait
|
||
|
|
- [ ] Implement LocalToolExecutor
|
||
|
|
- [ ] Implement AgentToolExecutor
|
||
|
|
- [ ] Add tool routing logic
|
||
|
|
|
||
|
|
### Week 4: Hybrid Loop
|
||
|
|
- [ ] Modify main loop for provider switching
|
||
|
|
- [ ] Add streaming TUI updates
|
||
|
|
- [ ] Implement automatic fallback
|
||
|
|
- [ ] Add mode indicator to UI
|
||
|
|
|
||
|
|
### Week 5: Testing & Docs
|
||
|
|
- [ ] Test all three modes (local, agent, hybrid)
|
||
|
|
- [ ] Add configuration examples
|
||
|
|
- [ ] Write migration guide
|
||
|
|
- [ ] Update README
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Conclusion
|
||
|
|
|
||
|
|
The **hybrid BotCoder** gives users the best of both worlds:
|
||
|
|
|
||
|
|
1. **CLI First** - Fast, local, privacy-focused development
|
||
|
|
2. **Multi-Agent Power** - On-demand access to 12 specialized agents
|
||
|
|
3. **Seamless Switching** - Automatic fallback between modes
|
||
|
|
4. **Progressive Enhancement** - Start simple, scale when needed
|
||
|
|
|
||
|
|
**Result:** A coding agent that works offline for quick fixes but can call in a full multi-agent orchestra when facing complex challenges.
|
||
|
|
|
||
|
|
**Estimated Effort:** 5 weeks (1 developer)
|
||
|
|
**Lines of Code:** ~2000 new lines (modular, trait-based)
|
||
|
|
|
||
|
|
The BotCoder CLI becomes the **control plane** for the Mantis Farm, offering both direct terminal access and a gateway to the full multi-agent OS when needed.
|