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:
122
crates/gyandoot-providers/src/anthropic.rs
Normal file
122
crates/gyandoot-providers/src/anthropic.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user