first commit
This commit is contained in:
commit
91b44fcc16
27 files changed
+4104
No files matched your search
@@ -0,0 +1 @@
|
||||
/target
|
||||
@@ -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 }
|
||||
@@ -0,0 +1,2 @@
|
||||
mod react;
|
||||
pub use react::{ReActAgent, ReActAgentBuilder};
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub enum AgentEvent {
|
||||
SessionCreated { session_id: SessionId },
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user