first commit

This commit is contained in:
aiyezi committed 2026-11-05 17:33:18 +08:00
commit 91b44fcc16
27 files changed
+4104

No files matched your search

+1
View File
@@ -0,0 +1 @@
/target
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "agent-core"
version = "0.1.0"
edition = "2024"
[dependencies]
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
async-trait = { workspace = true }
reqwest = { workspace = true }
+2
View File
@@ -0,0 +1,2 @@
mod react;
pub use react::{ReActAgent, ReActAgentBuilder};
+146
View File
@@ -0,0 +1,146 @@
use std::sync::Arc;
use crate::{
AgentError, ChatRequest, LlmProvider, Message, Role,
message::ToolCall,
provider::ToolSpce,
tool::{Tool, ToolRegistry},
};
pub struct ReActAgent {
provider: Arc<dyn LlmProvider>,
model: String,
tools: ToolRegistry,
system_prompt: Option<String>,
max_iterations: usize,
temperature: Option<f32>,
}
pub struct ReActOutput {
pub final_message: Message,
pub history: Vec<Message>,
pub iterations: usize,
}
impl ReActAgent {
pub fn builder(provider: Arc<dyn LlmProvider>, model: impl Into<String>) -> ReActAgentBuilder {
ReActAgentBuilder {
provider,
model: model.into(),
tools: ToolRegistry::new(),
system_prompt: None,
max_iterations: 5,
temperature: None,
}
}
pub async fn run(&self, user_input: impl Into<String>) -> Result<ReActOutput, AgentError> {
let mut history: Vec<Message> = Vec::new();
if let Some(prompt) = &self.system_prompt {
history.push(Message::system(prompt));
}
history.push(Message::user(user_input.into()));
let tool_specs: Vec<ToolSpce> = if self.tools.is_empty() {
Vec::new()
} else {
self.tools.spec()
};
for iteration in 0..self.max_iterations {
let request = ChatRequest {
model: self.model.clone(),
messages: history.clone(),
temperature: self.temperature,
max_tokens: None,
tools: tool_specs.clone(),
};
let response = self.provider.chat(request).await?;
if !response.tool_calls.is_empty() {
history.push(Message {
role: Role::Assistant,
content: response.content,
tool_calls: Some(response.tool_calls.clone()),
tool_call_id: None,
});
for call in &response.tool_calls {
let result = self.execute_tool(call).await;
history.push(Message::tool(result, call.id.clone()));
}
continue;
}
let content = response.content.unwrap_or_default();
let final_message = Message::assistant(content);
history.push(final_message.clone());
return Ok(ReActOutput {
final_message,
history,
iterations: iteration + 1,
});
}
Err(AgentError::Provider(format!(
"agent exceeded max_iterations {}",
self.max_iterations
)))
}
async fn execute_tool(&self, call: &ToolCall) -> String {
let Some(tool) = self.tools.get(&call.name) else {
return format!("Error: tool {} not found", call.name);
};
match tool.run(&call.arguments).await {
Ok(out) => out,
Err(e) => format!("Error executing tool {}: {}", call.name, e),
}
}
}
pub struct ReActAgentBuilder {
provider: Arc<dyn LlmProvider>,
model: String,
tools: ToolRegistry,
system_prompt: Option<String>,
max_iterations: usize,
temperature: Option<f32>,
}
impl ReActAgentBuilder {
pub fn tool(mut self, tool: Arc<dyn Tool>) -> Self {
self.tools.register(tool);
self
}
pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
self.system_prompt = Some(prompt.into());
self
}
pub fn max_iterations(mut self, n: usize) -> Self {
self.max_iterations = n;
self
}
pub fn temperature(mut self, temperature: f32) -> Self {
self.temperature = Some(temperature);
self
}
pub fn build(self) -> ReActAgent {
ReActAgent {
provider: self.provider,
tools: self.tools,
model: self.model,
system_prompt: self.system_prompt,
max_iterations: self.max_iterations,
temperature: self.temperature,
}
}
}
+22
View File
@@ -0,0 +1,22 @@
use thiserror::Error;
#[derive(Debug, Error)]
pub enum AgentError {
#[error("provider error: {0}")]
Provider(String),
#[error("http errpr: {0}")]
Http(#[from] reqwest::Error),
#[error("serialization error {0}")]
Serialization(#[from] serde_json::Error),
#[error("Api error {status}: {message}")]
Api { status: u16, message: String },
}
impl From<std::fmt::Error> for AgentError {
fn from(err: std::fmt::Error) -> Self {
AgentError::Provider(format!("write fail: {}", err))
}
}
+3
View File
@@ -0,0 +1,3 @@
pub enum AgentEvent {
SessionCreated { session_id: SessionId },
}
+27
View File
@@ -0,0 +1,27 @@
mod agent;
mod error;
mod event;
mod message;
mod provider;
mod tool;
pub use agent::{ReActAgent, ReActAgentBuilder};
pub use error::AgentError;
pub use message::{Message, Role, ToolCall};
pub use provider::{ChatRequest, ChatResponse, LlmProvider};
pub use tool::Tool;
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);
}
}
+59
View File
@@ -0,0 +1,59 @@
use serde::{Deserialize, Serialize, de};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum Role {
#[default]
System,
User,
Assistant,
Tool,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Message {
pub role: Role,
pub content: Option<String>,
pub tool_calls: Option<Vec<ToolCall>>,
pub tool_call_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCall {
pub id: String,
pub name: String,
pub arguments: String,
}
impl Message {
pub fn system(content: impl Into<String>) -> Self {
Self {
content: Some(content.into()),
..Default::default()
}
}
pub fn user(content: impl Into<String>) -> Self {
Self {
role: Role::User,
content: Some(content.into()),
..Default::default()
}
}
pub fn assistant(content: impl Into<String>) -> Self {
Self {
role: Role::Assistant,
content: Some(content.into()),
..Default::default()
}
}
pub fn tool(content: impl Into<String>, tool_call_id: impl Into<String>) -> Self {
Self {
role: Role::Tool,
content: Some(content.into()),
tool_call_id: Some(tool_call_id.into()),
..Default::default()
}
}
}
+34
View File
@@ -0,0 +1,34 @@
use crate::{
error::AgentError,
message::{Message, ToolCall},
};
#[derive(Debug, Clone)]
pub struct ChatRequest {
pub model: String,
pub messages: Vec<Message>,
pub temperature: Option<f32>,
pub max_tokens: Option<u32>,
pub tools: Vec<ToolSpce>,
}
#[derive(Debug, Clone)]
pub struct ChatResponse {
pub content: Option<String>,
pub model: String,
pub prompt_tokens: Option<u32>,
pub completion_tokens: Option<u32>,
pub tool_calls: Vec<ToolCall>,
}
#[derive(Debug, Clone)]
pub struct ToolSpce {
pub name: String,
pub description: String,
pub parameters: serde_json::Value,
}
#[async_trait::async_trait]
pub trait LlmProvider: Send + Sync {
async fn chat(&self, request: ChatRequest) -> Result<ChatResponse, AgentError>;
}
+47
View File
@@ -0,0 +1,47 @@
use std::{collections::HashMap, sync::Arc};
use async_trait::async_trait;
use crate::{AgentError, provider::ToolSpce};
#[async_trait]
pub trait Tool: Send + Sync {
fn name(&self) -> &str;
fn description(&self) -> &str;
fn parameters(&self) -> serde_json::Value;
async fn run(&self, arguments: &str) -> Result<String, AgentError>;
fn spec(&self) -> ToolSpce {
ToolSpce {
name: self.name().to_string(),
description: self.description().to_string(),
parameters: self.parameters(),
}
}
}
#[derive(Default, Clone)]
pub struct ToolRegistry {
tools: HashMap<String, Arc<dyn Tool>>,
}
impl ToolRegistry {
pub fn new() -> ToolRegistry {
Self::default()
}
pub fn register(&mut self, tool: Arc<dyn Tool>) {
self.tools.insert(tool.name().into(), tool);
}
pub fn get(&self, name: &str) -> Option<Arc<dyn Tool>> {
self.tools.get(name).cloned()
}
pub fn spec(&self) -> Vec<ToolSpce> {
self.tools.values().map(|t| t.spec()).collect()
}
pub fn is_empty(&self) -> bool {
self.tools.is_empty()
}
}