first commit
This commit is contained in:
commit
91b44fcc16
27 files changed
+4104
No files matched your search
@@ -0,0 +1,3 @@
|
|||||||
|
BASE_URL="https://api.deepseek.com"
|
||||||
|
API_KEY="sk-693aedfd60264fc696fe86736c3e7919"
|
||||||
|
MODEL="deepseek-v4-flash"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
target/
|
||||||
Generated
+3347
File diff suppressed because it is too large.
Load diff
+21
@@ -0,0 +1,21 @@
|
|||||||
|
[workspace]
|
||||||
|
resolver = "2"
|
||||||
|
members = [
|
||||||
|
"bin/agent-cli",
|
||||||
|
"crates/agent-core",
|
||||||
|
"crates/agent-providers",
|
||||||
|
"crates/agent-tools",
|
||||||
|
"crates/agent-tui",
|
||||||
|
]
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
tokio = { version = "1.53.1", features = ["full"] }
|
||||||
|
serde = { version = "1.0.229", features = ["derive"] }
|
||||||
|
serde_json = "1.0.151"
|
||||||
|
thiserror = "2.0.20"
|
||||||
|
async-trait = "0.1.92"
|
||||||
|
reqwest = { version = "0.13.5", features = ["json"] }
|
||||||
|
dotenvy = { version = "0.15.7" }
|
||||||
|
ratatui = "0.30.2"
|
||||||
|
crossterm = "0.29.0"
|
||||||
|
color-eyre = "0.6.5"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
[package]
|
||||||
|
name = "agent-cli"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tokio = { workspace = true }
|
||||||
Whitespace-only changes.
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
Submodule
+1
Submodule crates/agent-providers added at d360b650d8.
@@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "agent-tools"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
agent-core = { path = "../agent-core" }
|
||||||
|
async-trait = { workspace = true }
|
||||||
|
jiff = "0.2.37"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
mod read;
|
||||||
|
pub use read::ReadFileTool;
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
use std::{
|
||||||
|
fmt::Write,
|
||||||
|
fs::File,
|
||||||
|
io::{BufRead, BufReader},
|
||||||
|
};
|
||||||
|
|
||||||
|
use agent_core::{AgentError, Tool};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
pub struct ReadFileTool;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct ToolArgs {
|
||||||
|
pub path: String,
|
||||||
|
pub range: Option<(usize, usize)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Tool for ReadFileTool {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"read_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> &str {
|
||||||
|
"Reads the contents of a file at the specified path."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parameters(&self) -> serde_json::Value {
|
||||||
|
serde_json::json!({
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"path": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The path to the file to read."
|
||||||
|
},
|
||||||
|
"range": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Optional line range [start, count], Number of lines to read. For example, start=100, count=50 reads lines 100 to 149 (inclusive)",
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"maxItems": 2,
|
||||||
|
"minItems": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["path"]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run(&self, arguments: &str) -> Result<String, AgentError> {
|
||||||
|
let args: ToolArgs = serde_json::from_str(arguments)
|
||||||
|
.map_err(|e| AgentError::Provider(format!("Invalid arguments {e}")))?;
|
||||||
|
|
||||||
|
let file =
|
||||||
|
File::open(args.path).map_err(|e| AgentError::Provider(format!("Invalid path {e}")))?;
|
||||||
|
|
||||||
|
read_line(args.range, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_line(range: Option<(usize, usize)>, file: File) -> Result<String, AgentError> {
|
||||||
|
let reader = BufReader::new(file);
|
||||||
|
let mut ans = String::new();
|
||||||
|
|
||||||
|
match range {
|
||||||
|
Some((start, count)) => {
|
||||||
|
if start == 0 {
|
||||||
|
return Err(AgentError::Provider(format!(
|
||||||
|
"Invalid arguments: start must be >= 1, got {}",
|
||||||
|
start
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if count > 200 {
|
||||||
|
return Err(AgentError::Provider(format!(
|
||||||
|
"Invalid arguments: count must be <= 200, got {}",
|
||||||
|
count
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i, line) in reader.lines().skip(start - 1).take(count).enumerate() {
|
||||||
|
let line =
|
||||||
|
line.map_err(|e| AgentError::Provider(format!("failed to read file: {}", e)))?;
|
||||||
|
writeln!(ans, "[{}]: {}", start + i, line)?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// 明确上限,避免模型无界读取
|
||||||
|
const MAX_LINES: usize = 1000;
|
||||||
|
for (i, line) in reader.lines().take(MAX_LINES).enumerate() {
|
||||||
|
let line =
|
||||||
|
line.map_err(|e| AgentError::Provider(format!("failed to read file: {}", e)))?;
|
||||||
|
writeln!(ans, "[{}]: {}", i + 1, line)?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ans)
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
mod fs;
|
||||||
|
mod time;
|
||||||
|
pub use fs::ReadFileTool;
|
||||||
|
pub use time::CurrentTimeTool;
|
||||||
|
|
||||||
|
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,60 @@
|
|||||||
|
use agent_core::{AgentError, Tool};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use jiff::Zoned;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
pub struct CurrentTimeTool;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Default)]
|
||||||
|
pub struct TimeArgs {
|
||||||
|
#[serde(default)]
|
||||||
|
timezone: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Tool for CurrentTimeTool {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"get_current_time"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> &str {
|
||||||
|
"get system now time"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parameters(&self) -> serde_json::Value {
|
||||||
|
serde_json::json!({
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"timezone": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Optional IANA timezone name (e.g., 'Asia/Shanghai', 'America/New_York', 'UTC'). If not provided, the local system timezone is used."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run(&self, arguments: &str) -> Result<String, AgentError> {
|
||||||
|
let args: TimeArgs = if arguments.trim().is_empty() {
|
||||||
|
TimeArgs::default()
|
||||||
|
} else {
|
||||||
|
serde_json::from_str(arguments)
|
||||||
|
.map_err(|e| AgentError::Provider(format!("invaild argments {e}")))?
|
||||||
|
};
|
||||||
|
|
||||||
|
let now = Zoned::now();
|
||||||
|
let target_time = match args.timezone.as_deref() {
|
||||||
|
Some(tz) if !tz.trim().is_empty() => now
|
||||||
|
.in_tz(tz)
|
||||||
|
.map_err(|e| AgentError::Provider(format!("invalid timezone '{tz}': {e}")))?,
|
||||||
|
_ => now,
|
||||||
|
};
|
||||||
|
|
||||||
|
let formatted = format!(
|
||||||
|
"{} [{}]",
|
||||||
|
target_time.strftime("%Y-%m-%d %H:%M:%S %A"),
|
||||||
|
target_time.time_zone().iana_name().unwrap_or("Unknown")
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(formatted)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "agent-tui"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
agent-core = { path = "../agent-core" }
|
||||||
|
crossterm = { workspace = true }
|
||||||
|
ratatui = { workspace = true }
|
||||||
|
color-eyre = { workspace = true }
|
||||||
|
dotenvy = { workspace = true }
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use agent_core::ReActAgent;
|
||||||
|
use agent_providers::ChatCompletionsProvider;
|
||||||
|
use agent_tui::App;
|
||||||
|
use color_eyre::eyre::Ok;
|
||||||
|
|
||||||
|
fn main() -> color_eyre::Result<()> {
|
||||||
|
color_eyre::install()?;
|
||||||
|
let app = App::default();
|
||||||
|
app.run()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
mod ui;
|
||||||
|
pub use ui::App;
|
||||||
|
|
||||||
|
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,139 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crossterm::event::{Event, KeyCode, KeyEvent};
|
||||||
|
use ratatui::{
|
||||||
|
Frame,
|
||||||
|
layout::{Constraint, Direction, Layout},
|
||||||
|
text::Line,
|
||||||
|
widgets::{Block, List, ListItem, Paragraph},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct App {
|
||||||
|
messages: Vec<Message>,
|
||||||
|
input: String,
|
||||||
|
pending: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Message {
|
||||||
|
role: Role,
|
||||||
|
content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq)]
|
||||||
|
enum Role {
|
||||||
|
System,
|
||||||
|
User,
|
||||||
|
Assistant,
|
||||||
|
Tool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq)]
|
||||||
|
enum Flow {
|
||||||
|
Continue,
|
||||||
|
Quit,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
messages: vec![Message {
|
||||||
|
role: Role::System,
|
||||||
|
content: "hi there ~".into(),
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resume(messages: Vec<Message>) -> Self {
|
||||||
|
Self {
|
||||||
|
messages,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(mut self, act: ) -> std::io::Result<()> {
|
||||||
|
ratatui::run(|terminal| {
|
||||||
|
loop {
|
||||||
|
terminal.draw(|frame| self.render(frame))?;
|
||||||
|
if crossterm::event::poll(Duration::from_millis(16))? {
|
||||||
|
match crossterm::event::read()? {
|
||||||
|
Event::Key(key) => {
|
||||||
|
if self.handle_key(key) == Flow::Quit {
|
||||||
|
break Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_key(&mut self, key: KeyEvent) -> Flow {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Esc => Flow::Quit,
|
||||||
|
KeyCode::Backspace => {
|
||||||
|
self.input.pop();
|
||||||
|
Flow::Continue
|
||||||
|
}
|
||||||
|
KeyCode::Char(ch) => {
|
||||||
|
self.input.push(ch);
|
||||||
|
Flow::Continue
|
||||||
|
}
|
||||||
|
KeyCode::Enter => {
|
||||||
|
self.submit();
|
||||||
|
Flow::Continue
|
||||||
|
}
|
||||||
|
_ => Flow::Continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn submit(&mut self) {
|
||||||
|
self.messages.push(Message {
|
||||||
|
role: Role::User,
|
||||||
|
content: self.input.clone(),
|
||||||
|
});
|
||||||
|
self.input.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&self, frame: &mut Frame) {
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([
|
||||||
|
Constraint::Length(3),
|
||||||
|
Constraint::Min(0),
|
||||||
|
Constraint::Length(3),
|
||||||
|
])
|
||||||
|
.split(frame.area());
|
||||||
|
|
||||||
|
let header = Paragraph::new("Agent Chat").block(Block::bordered().title("Title"));
|
||||||
|
frame.render_widget(header, chunks[0]);
|
||||||
|
|
||||||
|
let items: Vec<ListItem> = self
|
||||||
|
.messages
|
||||||
|
.iter()
|
||||||
|
.map(|msg| {
|
||||||
|
let name = match msg.role {
|
||||||
|
Role::System => "System",
|
||||||
|
Role::User => "User",
|
||||||
|
Role::Assistant => "Assistant",
|
||||||
|
Role::Tool => "Tool",
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut lines = vec![Line::from(name)];
|
||||||
|
for line in msg.content.lines() {
|
||||||
|
lines.push(Line::from(line));
|
||||||
|
}
|
||||||
|
|
||||||
|
ListItem::new(lines)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let messages = List::new(items).block(Block::bordered().title("Messages"));
|
||||||
|
frame.render_widget(messages, chunks[1]);
|
||||||
|
|
||||||
|
let input = Paragraph::new(self.input.as_str()).block(Block::bordered().title("Input"));
|
||||||
|
frame.render_widget(input, chunks[2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user