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()
|
||||
}
|
||||
}
|
||||
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