Skip to content

Commit 87b6919

Browse files
committed
Add fluent-agent crate and agent CLI subcommand
1 parent 97ecec3 commit 87b6919

5 files changed

Lines changed: 127 additions & 1 deletion

File tree

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ edition = "2021"
66
[workspace]
77
members = [
88
"crates/fluent-cli",
9+
"crates/fluent-agent",
910
"crates/fluent-core",
1011
"crates/fluent-engines",
1112
"crates/fluent-storage",
@@ -19,6 +20,7 @@ fluent-core = { path = "crates/fluent-core" }
1920
fluent-engines = { path = "crates/fluent-engines" }
2021
fluent-storage = { path = "crates/fluent-storage" }
2122
fluent-sdk = { path = "crates/fluent-sdk" }
23+
fluent-agent = { path = "crates/fluent-agent" }
2224
tokio = { workspace = true, features = ["full"] }
2325
clap = { workspace = true, features = ["derive"] }
2426
anyhow = { workspace = true }
@@ -39,6 +41,7 @@ fluent-core = { path = "crates/fluent-core" }
3941
fluent-engines = { path = "crates/fluent-engines" }
4042
fluent-storage = { path = "crates/fluent-storage" }
4143
fluent-sdk = { path = "crates/fluent-sdk" }
44+
fluent-agent = { path = "crates/fluent-agent" }
4245
serde = { version = "^1.0" }
4346
lambda_runtime = "^0.13"
4447
serde_json = "^1.0"

crates/fluent-agent/Cargo.toml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[package]
2+
name = "fluent-agent"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
fluent-engines = { workspace = true }
8+
fluent-core = { workspace = true }
9+
anyhow = { workspace = true }
10+
tokio = { workspace = true, features = ["full"] }
11+
async-trait = { workspace = true }
12+
serde = { workspace = true, features = ["derive"] }
13+
serde_json = { workspace = true }
14+
chrono = { workspace = true }
15+
reqwest = { workspace = true }
16+
clap = { workspace = true }
17+

crates/fluent-agent/src/lib.rs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
use anyhow::{anyhow, Result};
2+
use fluent_core::traits::Engine;
3+
use fluent_core::types::Request;
4+
use std::path::Path;
5+
use std::process::Stdio;
6+
use tokio::fs;
7+
use tokio::process::Command;
8+
9+
/// Simple agent that keeps a history of prompt/response pairs.
10+
pub struct Agent {
11+
engine: Box<dyn Engine>,
12+
history: Vec<(String, String)>,
13+
}
14+
15+
impl Agent {
16+
/// Create a new agent from an engine.
17+
pub fn new(engine: Box<dyn Engine>) -> Self {
18+
Self { engine, history: Vec::new() }
19+
}
20+
21+
/// Send a prompt to the engine and store the response in history.
22+
pub async fn send(&mut self, prompt: &str) -> Result<String> {
23+
let request = Request { flowname: "agent".to_string(), payload: prompt.to_string() };
24+
let response = self.engine.execute(&request).await?;
25+
let content = response.content.clone();
26+
self.history.push((prompt.to_string(), content.clone()));
27+
Ok(content)
28+
}
29+
30+
/// Read a file asynchronously.
31+
pub async fn read_file(&self, path: &Path) -> Result<String> {
32+
Ok(fs::read_to_string(path).await?)
33+
}
34+
35+
/// Write a file asynchronously.
36+
pub async fn write_file(&self, path: &Path, content: &str) -> Result<()> {
37+
fs::write(path, content).await.map_err(Into::into)
38+
}
39+
40+
/// Run a shell command and capture stdout and stderr.
41+
pub async fn run_command(&self, cmd: &str, args: &[&str]) -> Result<String> {
42+
let output = Command::new(cmd)
43+
.args(args)
44+
.stdout(Stdio::piped())
45+
.stderr(Stdio::piped())
46+
.output()
47+
.await?;
48+
let mut result = String::from_utf8_lossy(&output.stdout).to_string();
49+
if !output.status.success() {
50+
result.push_str(&String::from_utf8_lossy(&output.stderr));
51+
}
52+
Ok(result)
53+
}
54+
55+
/// Commit changes in the current git repository.
56+
pub async fn git_commit(&self, message: &str) -> Result<()> {
57+
self.run_command("git", &["add", "."]).await?;
58+
let status = Command::new("git")
59+
.args(["commit", "-m", message])
60+
.status()
61+
.await?;
62+
if !status.success() {
63+
return Err(anyhow!("git commit failed"));
64+
}
65+
Ok(())
66+
}
67+
68+
/// Run a simple plan -> generate -> test -> commit cycle using the engine.
69+
pub async fn run_cycle(&mut self, prompt: &str) -> Result<()> {
70+
let plan = self.send(&format!("Plan: {}", prompt)).await?;
71+
let _generation = self.send(&format!("Generate code based on plan:\n{}", plan)).await?;
72+
73+
let test_output = self.run_command("cargo", &["test", "--quiet"]).await?;
74+
if !test_output.contains("0 failed") {
75+
return Err(anyhow!("tests failed"));
76+
}
77+
78+
self.git_commit(prompt).await?;
79+
Ok(())
80+
}
81+
}
82+

crates/fluent-cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ indicatif = { workspace = true }
1818
owo-colors = { workspace = true }
1919
regex = { workspace = true }
2020
serde_yaml = { workspace = true }
21+
fluent-agent = { path = "../fluent-agent" }
2122
#fluent-storage = { path = "../fluent-storage" } # is not used
2223
#clap_complete = "4.5.1" #is not used
2324
#atty = "0.2.14" "use standard std::io::IsTerminal"

crates/fluent-cli/src/lib.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ pub mod cli {
3434

3535
use log::{debug, error, info};
3636
use serde_json::Value;
37-
use tokio::io::AsyncReadExt;
37+
use tokio::io::{AsyncReadExt, AsyncBufReadExt, BufReader};
3838

3939
use crate::{create_llm_engine, generate_and_execute_cypher};
4040
use fluent_core::neo4j_client::{InteractionStats, Neo4jClient};
@@ -280,6 +280,10 @@ pub mod cli {
280280
.action(ArgAction::SetTrue),
281281
),
282282
)
283+
.subcommand(
284+
Command::new("agent")
285+
.about("Start interactive agent loop")
286+
)
283287
}
284288

285289
pub async fn get_neo4j_query_llm(config: &Config) -> Option<(Box<dyn Engine>, &EngineConfig)> {
@@ -363,6 +367,25 @@ pub mod cli {
363367
pb.enable_steady_tick(Duration::from_millis(spinner_config.interval));
364368
pb.set_length(100);
365369

370+
if matches.subcommand_matches("agent").is_some() {
371+
let engine: Box<dyn Engine> = create_engine(engine_config).await?;
372+
let mut agent = Agent::new(engine);
373+
let mut reader = BufReader::new(tokio::io::stdin());
374+
let mut line = String::new();
375+
println!("Starting agent loop. Type 'exit' to quit.");
376+
loop {
377+
line.clear();
378+
if reader.read_line(&mut line).await? == 0 { break; }
379+
let prompt = line.trim();
380+
if prompt.eq_ignore_ascii_case("exit") { break; }
381+
if prompt.is_empty() { continue; }
382+
if let Err(e) = agent.run_cycle(prompt).await {
383+
eprintln!("Agent error: {}", e);
384+
}
385+
}
386+
return Ok(());
387+
}
388+
366389
if let Some(cypher_query) = matches.get_one::<String>("generate-cypher") {
367390
let neo4j_config = engine_config
368391
.neo4j

0 commit comments

Comments
 (0)