From 339ca0f87eb3799243d5181f29ca1e70d72f220d Mon Sep 17 00:00:00 2001 From: aiyezi <424161078@qq.com> Date: Thu, 5 Nov 2026 17:36:42 +0800 Subject: [PATCH] recovery: change agent-providers as a directory --- crates/agent-providers | 1 - crates/agent-providers/.gitignore | 1 + crates/agent-providers/Cargo.toml | 15 +++ crates/agent-providers/examples/react.rs | 35 ++++++ .../src/chat_completions/client.rs | 38 ++++++ .../src/chat_completions/convert.rs | 110 ++++++++++++++++++ .../src/chat_completions/mod.rs | 45 +++++++ .../src/chat_completions/types.rs | 84 +++++++++++++ crates/agent-providers/src/lib.rs | 17 +++ 9 files changed, 345 insertions(+), 1 deletion(-) delete mode 160000 crates/agent-providers create mode 100644 crates/agent-providers/.gitignore create mode 100644 crates/agent-providers/Cargo.toml create mode 100644 crates/agent-providers/examples/react.rs create mode 100644 crates/agent-providers/src/chat_completions/client.rs create mode 100644 crates/agent-providers/src/chat_completions/convert.rs create mode 100644 crates/agent-providers/src/chat_completions/mod.rs create mode 100644 crates/agent-providers/src/chat_completions/types.rs create mode 100644 crates/agent-providers/src/lib.rs diff --git a/crates/agent-providers b/crates/agent-providers deleted file mode 160000 index d360b65..0000000 --- a/crates/agent-providers +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d360b650d80738eef46eefbbb57951605d13bf05 diff --git a/crates/agent-providers/.gitignore b/crates/agent-providers/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/crates/agent-providers/.gitignore @@ -0,0 +1 @@ +/target diff --git a/crates/agent-providers/Cargo.toml b/crates/agent-providers/Cargo.toml new file mode 100644 index 0000000..fc71521 --- /dev/null +++ b/crates/agent-providers/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "agent-providers" +version = "0.1.0" +edition = "2024" + +[dependencies] +agent-core = { path = "../agent-core" } +agent-tools = { path = "../agent-tools" } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +async-trait = { workspace = true } +reqwest = { workspace = true } +tokio = { workspace = true } +dotenvy = { workspace = true } diff --git a/crates/agent-providers/examples/react.rs b/crates/agent-providers/examples/react.rs new file mode 100644 index 0000000..bab1fcd --- /dev/null +++ b/crates/agent-providers/examples/react.rs @@ -0,0 +1,35 @@ +use std::sync::Arc; + +use agent_core::ReActAgent; +use agent_providers::ChatCompletionsProvider; +use agent_tools::{CurrentTimeTool, ReadFileTool}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + dotenvy::dotenv().ok(); + let provider = Arc::new(ChatCompletionsProvider::from_env()?); + let agent = ReActAgent::builder(provider, std::env::var("MODEL").unwrap_or_default()) + .system_prompt("你是小助手!") + .max_iterations(5) + .tool(Arc::new(CurrentTimeTool)) + .tool(Arc::new(ReadFileTool)) + .build(); + + let output = agent + .run("帮我看看 /home/aieyzi/Documents/prompts.txt 第二行里是什么, 你只看第二行。") + .await?; + + println!("-- after {} iteration(s) ---", output.iterations); + println!("{}", output.final_message.content.unwrap_or_default()); + + println!("-- full history ---"); + for (i, m) in output.history.iter().enumerate() { + println!("[{}] {:?}: {:?}", i, m.role, m.content); + if let Some(calls) = &m.tool_calls { + for call in calls { + println!("{} -> {}({})", call.id, call.name, call.arguments); + } + } + } + Ok(()) +} diff --git a/crates/agent-providers/src/chat_completions/client.rs b/crates/agent-providers/src/chat_completions/client.rs new file mode 100644 index 0000000..667481c --- /dev/null +++ b/crates/agent-providers/src/chat_completions/client.rs @@ -0,0 +1,38 @@ +use agent_core::AgentError; +use reqwest::Client; + +use crate::chat_completions::types::{ + ChatCompletionsErrorBody, ChatCompletionsErrorResponse, ChatCompletionsRequest, + ChatCompletionsResponse, +}; + +pub async fn send_chat( + client: &Client, + base_url: &str, + api_key: &str, + request: ChatCompletionsRequest, +) -> Result { + let url = format!("{}/chat/completions", base_url); + let response = client + .post(&url) + .header("Authorization", format!("Bearer {}", api_key)) + .header("Content-Type", "application/json") + .json(&request) + .send() + .await?; + + let status = response.status(); + if !status.is_success() { + let body = response.text().await.unwrap_or_default(); + let message = serde_json::from_str::(&body) + .map(|e| e.error.message) + .unwrap_or(body); + return Err(AgentError::Api { + status: status.as_u16(), + message, + }); + } + + let parsed: ChatCompletionsResponse = response.json().await?; + Ok(parsed) +} diff --git a/crates/agent-providers/src/chat_completions/convert.rs b/crates/agent-providers/src/chat_completions/convert.rs new file mode 100644 index 0000000..0e3a565 --- /dev/null +++ b/crates/agent-providers/src/chat_completions/convert.rs @@ -0,0 +1,110 @@ +use agent_core::{ChatRequest, ChatResponse, Message, Role, ToolCall}; + +use crate::chat_completions::types::{ + ChatCompletionsFunctionCall, ChatCompletionsFunctionDef, ChatCompletionsMessage, + ChatCompletionsRequest, ChatCompletionsResponse, ChatCompletionsTool, ChatCompletionsToolCall, +}; + +impl From for ChatCompletionsMessage { + fn from(message: Message) -> Self { + let role = match message.role { + Role::System => "system", + Role::User => "user", + Role::Assistant => "assistant", + Role::Tool => "tool", + }; + Self { + role: role.into(), + content: message.content.into(), + tool_calls: message.tool_calls.map(|calls| { + calls + .into_iter() + .map(ChatCompletionsToolCall::from) + .collect() + }), + tool_call_id: message.tool_call_id, + } + } +} + +impl From for ChatCompletionsToolCall { + fn from(call: ToolCall) -> Self { + Self { + id: call.id, + r#type: "function".to_string(), + function: ChatCompletionsFunctionCall { + name: call.name, + arguments: call.arguments, + }, + } + } +} + +impl From for ToolCall { + fn from(call: ChatCompletionsToolCall) -> Self { + Self { + id: call.id, + name: call.function.name, + arguments: call.function.arguments, + } + } +} + +impl From for ChatCompletionsRequest { + fn from(request: ChatRequest) -> Self { + Self { + model: request.model.into(), + messages: request + .messages + .into_iter() + .map(ChatCompletionsMessage::from) + .collect(), + temperature: request.temperature, + max_tokens: request.max_tokens, + tools: request + .tools + .into_iter() + .map(|tool| ChatCompletionsTool { + r#type: "function", + function: ChatCompletionsFunctionDef { + name: tool.name, + description: tool.description.into(), + parameters: tool.parameters, + }, + }) + .collect(), + } + } +} + +impl From for ChatResponse { + fn from(response: ChatCompletionsResponse) -> Self { + let ChatCompletionsResponse { + model, + choices, + usage, + } = response; + + let (content, tool_calls) = match choices.into_iter().next() { + Some(choice) => ( + choice.message.content, + choice + .message + .tool_calls + .unwrap_or_default() + .into_iter() + .map(ToolCall::from) + .collect(), + ), + None => (None, Vec::new()), + }; + + Self { + model, + content, + prompt_tokens: usage.as_ref().map(|u| u.prompt_tokens), + completion_tokens: usage.as_ref().map(|u| u.completion_tokens), + tool_calls: tool_calls.into_iter().map(ToolCall::from).collect(), + } + } +} diff --git a/crates/agent-providers/src/chat_completions/mod.rs b/crates/agent-providers/src/chat_completions/mod.rs new file mode 100644 index 0000000..13a6c31 --- /dev/null +++ b/crates/agent-providers/src/chat_completions/mod.rs @@ -0,0 +1,45 @@ +mod client; +mod convert; +mod types; + +use agent_core::{AgentError, ChatRequest, ChatResponse, LlmProvider}; +use async_trait::async_trait; +use reqwest::Client; + +use crate::chat_completions::{ + client::send_chat, + types::{ChatCompletionsRequest, ChatCompletionsResponse}, +}; + +pub struct ChatCompletionsProvider { + api_key: String, + base_url: String, + client: Client, +} + +impl ChatCompletionsProvider { + pub fn new(base_url: impl Into, api_key: impl Into) -> Self { + Self { + base_url: base_url.into(), + api_key: api_key.into(), + client: Client::new(), + } + } + + pub fn from_env() -> Result { + let base_url = std::env::var("BASE_URL") + .map_err(|_| AgentError::Provider("BASE_URL NOT SET".into()))?; + let api_key = + std::env::var("API_KEY").map_err(|_| AgentError::Provider("API_KEY NOT SET".into()))?; + Ok(Self::new(base_url, api_key)) + } +} + +#[async_trait] +impl LlmProvider for ChatCompletionsProvider { + async fn chat(&self, request: ChatRequest) -> Result { + let req = ChatCompletionsRequest::from(request); + let resp = send_chat(&self.client, &self.base_url, &self.api_key, req).await?; + Ok(ChatResponse::from(resp)) + } +} diff --git a/crates/agent-providers/src/chat_completions/types.rs b/crates/agent-providers/src/chat_completions/types.rs new file mode 100644 index 0000000..b3fc69c --- /dev/null +++ b/crates/agent-providers/src/chat_completions/types.rs @@ -0,0 +1,84 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize)] +pub struct ChatCompletionsRequest { + pub model: String, + pub messages: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub temperature: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub max_tokens: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub tools: Vec, +} + +#[derive(Serialize)] +pub struct ChatCompletionsMessage { + pub role: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub content: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub tool_calls: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub tool_call_id: Option, +} + +#[derive(Serialize)] +pub struct ChatCompletionsTool { + pub r#type: &'static str, + pub function: ChatCompletionsFunctionDef, +} + +#[derive(Serialize)] +pub struct ChatCompletionsFunctionDef { + pub name: String, + pub description: Option, + pub parameters: serde_json::Value, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct ChatCompletionsToolCall { + pub id: String, + pub r#type: String, + pub function: ChatCompletionsFunctionCall, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct ChatCompletionsFunctionCall { + pub name: String, + pub arguments: String, +} + +#[derive(Deserialize)] +pub struct ChatCompletionsResponse { + pub model: String, + pub choices: Vec, + pub usage: Option, +} + +#[derive(Deserialize)] +pub struct ChatCompletionsChoice { + pub message: ChatCompletionsResponseMessage, +} + +#[derive(Deserialize)] +pub struct ChatCompletionsResponseMessage { + pub content: Option, + pub tool_calls: Option>, +} + +#[derive(Deserialize)] +pub struct ChatCompletionsUsage { + pub prompt_tokens: u32, + pub completion_tokens: u32, +} + +#[derive(Deserialize)] +pub struct ChatCompletionsErrorResponse { + pub error: ChatCompletionsErrorBody, +} + +#[derive(Deserialize)] +pub struct ChatCompletionsErrorBody { + pub message: String, +} diff --git a/crates/agent-providers/src/lib.rs b/crates/agent-providers/src/lib.rs new file mode 100644 index 0000000..fabdadc --- /dev/null +++ b/crates/agent-providers/src/lib.rs @@ -0,0 +1,17 @@ +pub mod chat_completions; +pub use chat_completions::ChatCompletionsProvider; + +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +}