Initial scaffold: GyanDoot v2.0 in Rust

Full Cargo workspace with 11 crates:
- gyandoot-core: Shared types, config, error types
- gyandoot-agent: Agent loop, tool execution, context compression
- gyandoot-providers: LLM provider abstraction (OpenAI, Anthropic, Ollama)
- gyandoot-memory: MemPalace persistent memory system
- gyandoot-platform: Platform adapters (Telegram, Discord, Webhook, CLI)
- gyandoot-security: Credential vault, sanitizer, threat detector
- gyandoot-kanban: Task decomposition, swarm orchestration, diagnostics
- gyandoot-subagents: Sub-agent spawning and orchestration
- gyandoot-skills: Skills discovery, loading, preprocessing, bundles
- gyandoot-profile: Multi-agent profile manager
- gyandoot-gateway: Gateway sessions, routing, access control

Plus: CLI entry point with clap, cross-platform installer script,
KANBAN.md, .gnpl/AGENTS.md

Workspace compiles clean: cargo check passes with 0 errors.
This commit is contained in:
2026-06-11 20:04:12 +05:30
commit 851c838ca5
53 changed files with 8037 additions and 0 deletions

View File

@@ -0,0 +1,122 @@
//! Anthropic API provider implementation.
use async_trait::async_trait;
use gyandoot_core::{ConversationContext, MessageRole, ProviderResult};
use reqwest::Client;
use tracing::debug;
use crate::{Provider, ProviderResponse};
/// Provider for the Anthropic (Claude) API.
pub struct AnthropicProvider {
api_key: String,
model: String,
client: Client,
}
impl AnthropicProvider {
/// Create a new Anthropic provider.
pub fn new(api_key: String, model: String) -> Self {
Self {
api_key,
model,
client: Client::new(),
}
}
/// Build the messages array in Anthropic's format.
fn build_messages(&self, context: &ConversationContext) -> Vec<serde_json::Value> {
context
.messages
.iter()
.filter(|m| m.role != MessageRole::System)
.map(|m| {
let role = match m.role {
MessageRole::User => "user",
MessageRole::Assistant => "assistant",
MessageRole::Tool => "user",
MessageRole::System => "user", // filtered above
};
serde_json::json!({
"role": role,
"content": m.content,
})
})
.collect()
}
/// Extract the system prompt from context (Anthropic uses a separate field).
fn extract_system(&self, context: &ConversationContext) -> String {
context
.messages
.iter()
.find(|m| m.role == MessageRole::System)
.map(|m| m.content.clone())
.unwrap_or_default()
}
}
#[async_trait]
impl Provider for AnthropicProvider {
async fn chat(&self, context: ConversationContext) -> ProviderResult<ProviderResponse> {
debug!("Anthropic chat request (model: {})", self.model);
let system = self.extract_system(&context);
let messages = self.build_messages(&context);
let mut body = serde_json::json!({
"model": self.model,
"messages": messages,
"max_tokens": context.max_tokens,
});
if !system.is_empty() {
body["system"] = serde_json::Value::String(system);
}
let response = self
.client
.post("https://api.anthropic.com/v1/messages")
.header("x-api-key", &self.api_key)
.header("anthropic-version", "2023-06-01")
.json(&body)
.send()
.await?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
return Err(gyandoot_core::ProviderError::Api(format!(
"Anthropic API error ({}): {}",
status, error_text
)));
}
let data: serde_json::Value = response.json().await?;
let content = data["content"][0]["text"]
.as_str()
.unwrap_or_default()
.to_string();
let finish_reason = data["stop_reason"].as_str().unwrap_or("end_turn").to_string();
Ok(ProviderResponse {
content,
tool_calls: None,
finish_reason,
})
}
async fn chat_stream(
&self,
context: ConversationContext,
) -> ProviderResult<Box<dyn tokio_stream::Stream<Item = ProviderResult<String>> + Send + Unpin>>
{
// Fall back to non-streaming for now
let result = self.chat(context).await?;
let stream = tokio_stream::StreamExt::chain(
tokio_stream::once(Ok(result.content)),
tokio_stream::empty(),
);
Ok(Box::new(stream))
}
}