feat(v2): complete multi-LLM providers, TUI redesign, and advanced agent features
Multi-LLM Provider Support: - Add llm-core crate with LlmProvider trait abstraction - Implement Anthropic Claude API client with streaming - Implement OpenAI API client with streaming - Add token counting with SimpleTokenCounter and ClaudeTokenCounter - Add retry logic with exponential backoff and jitter Borderless TUI Redesign: - Rewrite theme system with terminal capability detection (Full/Unicode256/Basic) - Add provider tabs component with keybind switching [1]/[2]/[3] - Implement vim-modal input (Normal/Insert/Visual/Command modes) - Redesign chat panel with timestamps and streaming indicators - Add multi-provider status bar with cost tracking - Add Nerd Font icons with graceful ASCII fallbacks - Add syntax highlighting (syntect) and markdown rendering (pulldown-cmark) Advanced Agent Features: - Add system prompt builder with configurable components - Enhance subagent orchestration with parallel execution - Add git integration module for safe command detection - Add streaming tool results via channels - Expand tool set: AskUserQuestion, TodoWrite, LS, MultiEdit, BashOutput, KillShell - Add WebSearch with provider abstraction Plugin System Enhancement: - Add full agent definition parsing from YAML frontmatter - Add skill system with progressive disclosure - Wire plugin hooks into HookManager 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,8 @@ serde_json = "1"
|
||||
color-eyre = "0.6"
|
||||
url = "2.5"
|
||||
async-trait = "0.1"
|
||||
scraper = "0.18"
|
||||
urlencoding = "2.1"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.39", features = ["macros", "rt-multi-thread"] }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use color_eyre::eyre::{Result, eyre};
|
||||
use reqwest::redirect::Policy;
|
||||
use scraper::{Html, Selector};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
use url::Url;
|
||||
@@ -173,6 +174,91 @@ impl SearchProvider for StubSearchProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/// DuckDuckGo HTML search provider
|
||||
pub struct DuckDuckGoSearchProvider {
|
||||
client: reqwest::Client,
|
||||
max_results: usize,
|
||||
}
|
||||
|
||||
impl DuckDuckGoSearchProvider {
|
||||
/// Create a new DuckDuckGo search provider with default max results (10)
|
||||
pub fn new() -> Self {
|
||||
Self::with_max_results(10)
|
||||
}
|
||||
|
||||
/// Create a new DuckDuckGo search provider with custom max results
|
||||
pub fn with_max_results(max_results: usize) -> Self {
|
||||
let client = reqwest::Client::builder()
|
||||
.user_agent("Mozilla/5.0 (compatible; Owlen/1.0)")
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
Self { client, max_results }
|
||||
}
|
||||
|
||||
/// Parse DuckDuckGo HTML results
|
||||
fn parse_results(html: &str, max_results: usize) -> Result<Vec<SearchResult>> {
|
||||
let document = Html::parse_document(html);
|
||||
|
||||
// DuckDuckGo HTML selectors
|
||||
let result_selector = Selector::parse(".result").map_err(|e| eyre!("Invalid selector: {:?}", e))?;
|
||||
let title_selector = Selector::parse(".result__title a").map_err(|e| eyre!("Invalid selector: {:?}", e))?;
|
||||
let snippet_selector = Selector::parse(".result__snippet").map_err(|e| eyre!("Invalid selector: {:?}", e))?;
|
||||
|
||||
let mut results = Vec::new();
|
||||
|
||||
for result in document.select(&result_selector).take(max_results) {
|
||||
let title = result
|
||||
.select(&title_selector)
|
||||
.next()
|
||||
.map(|e| e.text().collect::<String>().trim().to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
let url = result
|
||||
.select(&title_selector)
|
||||
.next()
|
||||
.and_then(|e| e.value().attr("href"))
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
|
||||
let snippet = result
|
||||
.select(&snippet_selector)
|
||||
.next()
|
||||
.map(|e| e.text().collect::<String>().trim().to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
if !title.is_empty() && !url.is_empty() {
|
||||
results.push(SearchResult { title, url, snippet });
|
||||
}
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DuckDuckGoSearchProvider {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl SearchProvider for DuckDuckGoSearchProvider {
|
||||
fn name(&self) -> &str {
|
||||
"duckduckgo"
|
||||
}
|
||||
|
||||
async fn search(&self, query: &str) -> Result<Vec<SearchResult>> {
|
||||
let encoded_query = urlencoding::encode(query);
|
||||
let url = format!("https://html.duckduckgo.com/html/?q={}", encoded_query);
|
||||
|
||||
let response = self.client.get(&url).send().await?;
|
||||
let html = response.text().await?;
|
||||
|
||||
Self::parse_results(&html, self.max_results)
|
||||
}
|
||||
}
|
||||
|
||||
/// WebSearch client with pluggable providers
|
||||
pub struct WebSearchClient {
|
||||
provider: Box<dyn SearchProvider>,
|
||||
@@ -192,6 +278,20 @@ impl WebSearchClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// Format search results for LLM consumption (markdown format)
|
||||
pub fn format_search_results(results: &[SearchResult]) -> String {
|
||||
if results.is_empty() {
|
||||
return "No results found.".to_string();
|
||||
}
|
||||
|
||||
results
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, r)| format!("{}. [{}]({})\n {}", i + 1, r.title, r.url, r.snippet))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n\n")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user