initial import

This commit is contained in:
Dhanji Prasanna
2025-09-05 13:01:25 +10:00
parent 8f9b46a189
commit 57626042a9
19 changed files with 4300 additions and 0 deletions

2471
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

43
Cargo.toml Normal file
View File

@@ -0,0 +1,43 @@
[workspace]
members = [
"crates/g3-cli",
"crates/g3-core",
"crates/g3-providers",
"crates/g3-config",
"crates/g3-execution"
]
resolver = "2"
[workspace.dependencies]
# Async runtime
tokio = { version = "1.0", features = ["full"] }
# HTTP client
reqwest = { version = "0.11", features = ["json", "stream"] }
# Serialization
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
# CLI
clap = { version = "4.0", features = ["derive"] }
# Error handling
anyhow = "1.0"
thiserror = "1.0"
# Logging
tracing = "0.1"
tracing-subscriber = "0.3"
# Configuration
config = "0.14"
# Utilities
uuid = { version = "1.0", features = ["v4"] }
[package]
name = "g3"
version = "0.1.0"
edition = "2021"
authors = ["G3 Team"]
description = "A general purpose AI agent that helps you complete tasks by writing code"
license = "MIT"
[dependencies]
g3-cli = { path = "crates/g3-cli" }
tokio = { workspace = true }
anyhow = { workspace = true }

93
DESIGN.md Normal file
View File

@@ -0,0 +1,93 @@
# G3 General Purpose AI Agent - Design Document
## Overview
G3 is a **code-first AI agent** that helps you complete tasks by writing and executing code or scripts. Instead of just giving advice, G3 solves problems by generating executable code in the appropriate language.
## Core Principles
1. **Code-First Philosophy**: Always try to solve problems with executable code
2. **Multi-Language Support**: Generate scripts in Python, Bash, JavaScript, Rust, etc.
3. **Unix Philosophy**: Small, focused tools that do one thing well
4. **Modularity**: Clear separation of concerns
5. **Composability**: Components can be combined in different ways
6. **Performance**: Blazing fast execution
## Architecture
### High-Level Components
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ CLI Module │ │ Core Engine │ │ LLM Providers │
│ │ │ │ │ │
│ - Task commands │◄──►│ - Task │◄──►│ - OpenAI │
│ - Interactive │ │ interpretation│ │ - Anthropic │
│ mode │ │ - Code │ │ - Local models │
│ - Code exec │ │ generation │ │ - Custom APIs │
│ approval │ │ - Script │ │ │
│ │ │ execution │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
└───────────────────────┼───────────────────────┘
┌─────────────────┐
│ Execution │
│ Engine │
│ │
│ - Python │
│ - Bash/Shell │
│ - JavaScript │
│ - Rust │
│ - Sandboxing │
└─────────────────┘
```
### Module Breakdown
#### 1. CLI Module (`g3-cli`)
- **Responsibility**: User interface and task interpretation
- **New Features**:
- Progress indicators for script execution
#### 2. Core Engine (`g3-core`)
- **Responsibility**: Task interpretation and code generation
- **New Features**:
- Task analysis and decomposition
- Language selection based on task type
- Code generation with execution context
- Script template system
- Autonomous execution of generated code
#### 3. LLM Providers (`g3-providers`)
- **Responsibility**: LLM communication (unchanged)
- **Enhanced Prompts**:
- Code-first system prompts
- Language-specific generation instructions
#### 4. Execution Engine (`g3-execution`) - NEW
- **Responsibility**: Safe code execution
- **Features**:
- Multi-language script execution
- Sandboxing and security
- Resource limits
- Output capture and formatting
- Error handling and recovery
### Task Types and Language Selection
| Task Type | Preferred Language | Use Cases |
|-----------|-------------------|-----------|
| Data Processing | Python | CSV/JSON analysis, data transformation |
| File Operations | Bash/Shell | File manipulation, backups, organization |
| System Admin | Bash/Shell | Process management, system monitoring |
| Text Processing | Python/Bash | Log analysis, text transformation |
| Database | Python/SQL | Data migration, queries, reporting |
| Image/Media | Python | Image processing, format conversion |
| Development | Rust | Code generation, project setup |
## Implementation Plan
### Phase 1: Core Refactoring
1. Update CLI commands for task-oriented interface
2. Enhance system prompts for code-first approach
3. Add basic code execution capabilities
4. Update interactive mode messaging

133
README.md Normal file
View File

@@ -0,0 +1,133 @@
# G3 General Purpose AI Agent
A code-first AI agent that helps you complete tasks by writing and executing code or scripts.
## Philosophy
G3 doesn't just give you advice - **it writes and runs code to solve your problems**. Whether you need to:
- Process data files
- Automate workflows
- Scrape websites
- Manipulate files
- Analyze logs
- Generate reports
- Set up environments
G3 will write the appropriate scripts (Python, Bash, JavaScript, etc.) and can execute them for you.
## Features
- **Code-First Approach**: Always tries to solve problems with executable code
- **Multi-Language Support**: Generates Python, Bash, JavaScript, Rust, and more
- **Modular Architecture**: Clean separation between CLI, core engine, and LLM providers
- **Multiple LLM Providers**: Support for OpenAI, Anthropic, and extensible to other providers
- **Interactive Mode**: Chat with the AI and watch it solve problems in real-time
- **Task Automation**: Create reusable automation scripts
## Installation
```bash
cargo install --path .
```
## Configuration
Create a configuration file at `~/.config/g3/config.toml`:
```toml
[providers]
default_provider = "openai"
[providers.openai]
api_key = "your-openai-api-key"
model = "gpt-4"
max_tokens = 2048
temperature = 0.1
[providers.anthropic]
api_key = "your-anthropic-api-key"
model = "claude-3-sonnet-20240229"
max_tokens = 2048
temperature = 0.1
[agent]
max_context_length = 8192
enable_streaming = true
timeout_seconds = 60
```
## Usage
### Interactive Mode (Default)
Simply run G3 to start interactive mode:
```bash
g3
```
Example interactions:
```
g3> Process this CSV file and show me the top 10 customers by revenue
# G3 writes Python pandas script to analyze the CSV
g3> Set up a backup script for my home directory
# G3 creates a bash script with rsync/tar commands
g3> Download all images from this webpage
# G3 writes a Python script with requests/BeautifulSoup
g3> Convert these JSON files to a SQLite database
# G3 creates a Python script to parse JSON and insert into SQLite
```
### Direct Commands
You can also use G3 with direct commands:
```bash
# Solve any task with code
g3 task "merge these PDF files into one"
g3 task "find all TODO comments in my codebase"
# Create automation scripts
g3 automate "daily backup of my projects folder"
g3 automate "resize all images in a folder to 800px width"
# Data processing
g3 data "analyze this log file for error patterns"
g3 data "convert CSV to JSON with validation"
# Legacy code commands (still supported)
g3 analyze src/main.rs
g3 generate "fibonacci function" --output fib.py
g3 review src/lib.rs
```
## Architecture
G3 follows the Unix philosophy with modular, composable components:
- **g3-cli**: Command-line interface
- **g3-core**: Core agent logic and orchestration
- **g3-providers**: LLM provider abstractions
- **g3-config**: Configuration management
See [DESIGN.md](DESIGN.md) for detailed architecture documentation.
## Development
```bash
# Build all crates
cargo build
# Run tests
cargo test
# Run with debug logging
RUST_LOG=debug cargo run -- analyze src/
```
## License
MIT

26
config.example.toml Normal file
View File

@@ -0,0 +1,26 @@
# Example configuration file for G3
# Copy to ~/.config/g3/config.toml and customize
[providers]
default_provider = "openai"
[providers.openai]
# Get your API key from https://platform.openai.com/api-keys
api_key = "sk-your-openai-api-key-here"
model = "gpt-4"
# Optional: custom base URL for OpenAI-compatible APIs
# base_url = "https://api.openai.com/v1"
max_tokens = 2048
temperature = 0.1
[providers.anthropic]
# Get your API key from https://console.anthropic.com/
api_key = "your-anthropic-api-key-here"
model = "claude-3-sonnet-20240229"
max_tokens = 2048
temperature = 0.1
[agent]
max_context_length = 8192
enable_streaming = true
timeout_seconds = 60

16
crates/g3-cli/Cargo.toml Normal file
View File

@@ -0,0 +1,16 @@
[package]
name = "g3-cli"
version = "0.1.0"
edition = "2021"
description = "CLI interface for G3 AI coding agent"
[dependencies]
g3-core = { path = "../g3-core" }
g3-config = { path = "../g3-config" }
clap = { workspace = true }
tokio = { workspace = true }
anyhow = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }

256
crates/g3-cli/src/lib.rs Normal file
View File

@@ -0,0 +1,256 @@
use clap::{Parser, Subcommand};
use g3_core::Agent;
use g3_config::Config;
use anyhow::Result;
use tracing::{info, error};
#[derive(Parser)]
#[command(name = "g3")]
#[command(about = "A modular, composable AI coding agent")]
#[command(version)]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Commands>,
/// Enable verbose logging
#[arg(short, long)]
pub verbose: bool,
/// Show the system prompt being sent to the LLM
#[arg(long)]
pub show_prompt: bool,
/// Show the generated code before execution
#[arg(long)]
pub show_code: bool,
/// Configuration file path
#[arg(short, long)]
pub config: Option<String>,
}
#[derive(Subcommand)]
pub enum Commands {
/// Solve any task by writing and executing code
Task {
/// Description of the task to accomplish
description: String,
/// Programming language to prefer (auto-detect if not specified)
#[arg(short, long)]
language: Option<String>,
/// Execute the generated code automatically (default: ask for approval)
#[arg(short, long)]
execute: bool,
},
/// Create automation scripts for recurring tasks
Automate {
/// Description of the workflow to automate
workflow: String,
/// Output file for the automation script
#[arg(short, long)]
output: Option<String>,
},
/// Process and analyze data with code
Data {
/// Description of the data processing task
operation: String,
/// Input file or data source
#[arg(short, long)]
input: Option<String>,
/// Output format (json, csv, text)
#[arg(short = 'f', long, default_value = "text")]
format: String,
},
/// Web-related tasks (scraping, APIs, downloads)
Web {
/// Description of the web task
task: String,
/// Target URL (if applicable)
#[arg(short, long)]
url: Option<String>,
},
/// File system operations and management
File {
/// Description of the file operation
operation: String,
/// Target path
#[arg(short, long)]
path: Option<String>,
},
/// Legacy: Analyze code and provide insights
Analyze {
/// Path to analyze
path: String,
/// Output format (json, text)
#[arg(short, long, default_value = "text")]
format: String,
},
/// Legacy: Generate code based on description
Generate {
/// Description of what to generate
description: String,
/// Output file path
#[arg(short, long)]
output: Option<String>,
},
/// Legacy: Review code and suggest improvements
Review {
/// Path to review
path: String,
},
/// Interactive mode (default)
Interactive,
}
pub async fn run() -> Result<()> {
let cli = Cli::parse();
// Initialize logging
let level = if cli.verbose {
tracing::Level::DEBUG
} else {
tracing::Level::INFO
};
tracing_subscriber::fmt()
.with_max_level(level)
.init();
info!("Starting G3 AI Coding Agent");
// Load configuration
let config = Config::load(cli.config.as_deref())?;
// Initialize agent
let agent = Agent::new(config).await?;
// Execute command - default to Interactive if no command provided
match cli.command.unwrap_or(Commands::Interactive) {
Commands::Task { description, language, execute } => {
info!("Executing task: {}", description);
let result = agent.execute_task(&description, language.as_deref(), execute).await?;
println!("{}", result);
}
Commands::Automate { workflow, output } => {
info!("Creating automation: {}", workflow);
let result = agent.create_automation(&workflow).await?;
if let Some(output_path) = output {
std::fs::write(&output_path, &result)?;
println!("Automation script written to: {}", output_path);
} else {
println!("{}", result);
}
}
Commands::Data { operation, input, format } => {
info!("Processing data: {}", operation);
let result = agent.process_data(&operation, input.as_deref()).await?;
match format.as_str() {
"json" => println!("{}", serde_json::to_string_pretty(&result)?),
_ => println!("{}", result),
}
}
Commands::Web { task, url } => {
info!("Web task: {}", task);
let result = agent.execute_web_task(&task, url.as_deref()).await?;
println!("{}", result);
}
Commands::File { operation, path } => {
info!("File operation: {}", operation);
let result = agent.execute_file_operation(&operation, path.as_deref()).await?;
println!("{}", result);
}
Commands::Analyze { path, format } => {
info!("Analyzing: {}", path);
let result = agent.analyze(&path).await?;
match format.as_str() {
"json" => println!("{}", serde_json::to_string_pretty(&result)?),
_ => println!("{}", result),
}
}
Commands::Generate { description, output } => {
info!("Generating code: {}", description);
let result = agent.generate(&description).await?;
if let Some(output_path) = output {
std::fs::write(&output_path, &result)?;
println!("Generated code written to: {}", output_path);
} else {
println!("{}", result);
}
}
Commands::Review { path } => {
info!("Reviewing: {}", path);
let result = agent.review(&path).await?;
println!("{}", result);
}
Commands::Interactive => {
info!("Starting interactive mode");
run_interactive(agent, cli.show_prompt, cli.show_code).await?;
}
}
Ok(())
}
async fn run_interactive(agent: Agent, show_prompt: bool, show_code: bool) -> Result<()> {
println!("🤖 G3 General Purpose AI Agent - Interactive Mode");
println!("I solve problems by writing code. Tell me what you need to accomplish!");
println!();
println!("Type 'exit' or 'quit' to exit");
println!();
loop {
print!("g3> ");
use std::io::{self, Write};
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let input = input.trim();
if input == "exit" || input == "quit" {
break;
}
if input.is_empty() {
continue;
}
// Handle legacy commands
if let Some(path) = input.strip_prefix("analyze ") {
match agent.analyze(path).await {
Ok(result) => println!("{}", result),
Err(e) => error!("Error analyzing {}: {}", path, e),
}
continue;
}
if let Some(description) = input.strip_prefix("generate ") {
match agent.generate(description).await {
Ok(result) => println!("{}", result),
Err(e) => error!("Error generating code: {}", e),
}
continue;
}
if let Some(path) = input.strip_prefix("review ") {
match agent.review(path).await {
Ok(result) => println!("{}", result),
Err(e) => error!("Error reviewing {}: {}", path, e),
}
continue;
}
// Default to task execution (code-first approach)
match agent.execute_task_with_timing(input, None, false, show_prompt, show_code, true).await {
Ok(response) => println!("{}", response),
Err(e) => error!("Error: {}", e),
}
}
println!("👋 Goodbye!");
Ok(())
}

View File

@@ -0,0 +1,13 @@
[package]
name = "g3-config"
version = "0.1.0"
edition = "2021"
description = "Configuration management for G3 AI coding agent"
[dependencies]
config = { workspace = true }
serde = { workspace = true }
anyhow = { workspace = true }
thiserror = { workspace = true }
toml = "0.8"
shellexpand = "3.0"

103
crates/g3-config/src/lib.rs Normal file
View File

@@ -0,0 +1,103 @@
use serde::{Deserialize, Serialize};
use anyhow::Result;
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
pub providers: ProvidersConfig,
pub agent: AgentConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProvidersConfig {
pub openai: Option<OpenAIConfig>,
pub anthropic: Option<AnthropicConfig>,
pub default_provider: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpenAIConfig {
pub api_key: String,
pub model: String,
pub base_url: Option<String>,
pub max_tokens: Option<u32>,
pub temperature: Option<f32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnthropicConfig {
pub api_key: String,
pub model: String,
pub max_tokens: Option<u32>,
pub temperature: Option<f32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentConfig {
pub max_context_length: usize,
pub enable_streaming: bool,
pub timeout_seconds: u64,
}
impl Default for Config {
fn default() -> Self {
Self {
providers: ProvidersConfig {
openai: None,
anthropic: None,
default_provider: "openai".to_string(),
},
agent: AgentConfig {
max_context_length: 8192,
enable_streaming: true,
timeout_seconds: 60,
},
}
}
}
impl Config {
pub fn load(config_path: Option<&str>) -> Result<Self> {
let mut settings = config::Config::builder();
// Load default configuration
settings = settings.add_source(config::Config::try_from(&Config::default())?);
// Load from config file if provided
if let Some(path) = config_path {
if Path::new(path).exists() {
settings = settings.add_source(config::File::with_name(path));
}
} else {
// Try to load from default locations
let default_paths = [
"./g3.toml",
"~/.config/g3/config.toml",
"~/.g3.toml",
];
for path in &default_paths {
let expanded_path = shellexpand::tilde(path);
if Path::new(expanded_path.as_ref()).exists() {
settings = settings.add_source(config::File::with_name(expanded_path.as_ref()));
break;
}
}
}
// Override with environment variables
settings = settings.add_source(
config::Environment::with_prefix("G3")
.separator("_")
);
let config = settings.build()?.try_deserialize()?;
Ok(config)
}
pub fn save(&self, path: &str) -> Result<()> {
let toml_string = toml::to_string_pretty(self)?;
std::fs::write(path, toml_string)?;
Ok(())
}
}

20
crates/g3-core/Cargo.toml Normal file
View File

@@ -0,0 +1,20 @@
[package]
name = "g3-core"
version = "0.1.0"
edition = "2021"
description = "Core engine for G3 AI coding agent"
[dependencies]
g3-providers = { path = "../g3-providers" }
g3-config = { path = "../g3-config" }
g3-execution = { path = "../g3-execution" }
tokio = { workspace = true }
reqwest = { workspace = true }
anyhow = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
uuid = { workspace = true }
async-trait = "0.1"
tokio-stream = "0.1"

470
crates/g3-core/src/lib.rs Normal file
View File

@@ -0,0 +1,470 @@
use g3_providers::{ProviderRegistry, CompletionRequest, Message, MessageRole};
use g3_config::Config;
use g3_execution::CodeExecutor;
use anyhow::Result;
use serde::{Serialize, Deserialize};
use std::path::Path;
use std::time::{Duration, Instant};
use tracing::info;
pub struct Agent {
providers: ProviderRegistry,
config: Config,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AnalysisResult {
pub summary: String,
pub issues: Vec<Issue>,
pub suggestions: Vec<Suggestion>,
pub metrics: CodeMetrics,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Issue {
pub severity: IssueSeverity,
pub message: String,
pub line: Option<u32>,
pub column: Option<u32>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum IssueSeverity {
Error,
Warning,
Info,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Suggestion {
pub description: String,
pub code_example: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CodeMetrics {
pub lines_of_code: u32,
pub complexity_score: f32,
pub maintainability_index: f32,
}
impl Agent {
pub async fn new(config: Config) -> Result<Self> {
let mut providers = ProviderRegistry::new();
// Register providers based on configuration
if let Some(openai_config) = &config.providers.openai {
let openai_provider = crate::providers::openai::OpenAIProvider::new(
openai_config.api_key.clone(),
openai_config.model.clone(),
openai_config.base_url.clone(),
)?;
providers.register(openai_provider);
}
if let Some(anthropic_config) = &config.providers.anthropic {
let anthropic_provider = crate::providers::anthropic::AnthropicProvider::new(
anthropic_config.api_key.clone(),
anthropic_config.model.clone(),
)?;
providers.register(anthropic_provider);
}
// Set default provider
providers.set_default(&config.providers.default_provider)?;
Ok(Self { providers, config })
}
pub async fn analyze(&self, path: &str) -> Result<AnalysisResult> {
info!("Analyzing path: {}", path);
let content = self.read_file_or_directory(path)?;
let provider = self.providers.get(None)?;
let messages = vec![
Message {
role: MessageRole::System,
content: "You are a code analysis expert. Analyze the provided code and return a detailed analysis including issues, suggestions, and metrics.".to_string(),
},
Message {
role: MessageRole::User,
content: format!("Please analyze this code:\n\n{}", content),
},
];
let request = CompletionRequest {
messages,
max_tokens: Some(2048),
temperature: Some(0.1),
stream: false,
};
let response = provider.complete(request).await?;
// For now, return a simplified analysis
// In a real implementation, we'd parse the LLM response into structured data
Ok(AnalysisResult {
summary: response.content,
issues: vec![],
suggestions: vec![],
metrics: CodeMetrics {
lines_of_code: content.lines().count() as u32,
complexity_score: 1.0,
maintainability_index: 85.0,
},
})
}
pub async fn generate(&self, description: &str) -> Result<String> {
info!("Generating code for: {}", description);
let provider = self.providers.get(None)?;
let messages = vec![
Message {
role: MessageRole::System,
content: "You are a code generation expert. Generate clean, well-documented code based on the user's description.".to_string(),
},
Message {
role: MessageRole::User,
content: description.to_string(),
},
];
let request = CompletionRequest {
messages,
max_tokens: Some(2048),
temperature: Some(0.2),
stream: false,
};
let response = provider.complete(request).await?;
Ok(response.content)
}
pub async fn review(&self, path: &str) -> Result<String> {
info!("Reviewing path: {}", path);
let content = self.read_file_or_directory(path)?;
let provider = self.providers.get(None)?;
let messages = vec![
Message {
role: MessageRole::System,
content: "You are a code review expert. Review the provided code and suggest improvements focusing on best practices, performance, and maintainability.".to_string(),
},
Message {
role: MessageRole::User,
content: format!("Please review this code:\n\n{}", content),
},
];
let request = CompletionRequest {
messages,
max_tokens: Some(2048),
temperature: Some(0.1),
stream: false,
};
let response = provider.complete(request).await?;
Ok(response.content)
}
pub async fn execute_task(&self, description: &str, language: Option<&str>, _auto_execute: bool) -> Result<String> {
self.execute_task_with_options(description, language, false, false, false).await
}
pub async fn execute_task_with_options(&self, description: &str, language: Option<&str>, _auto_execute: bool, show_prompt: bool, show_code: bool) -> Result<String> {
self.execute_task_with_timing(description, language, _auto_execute, show_prompt, show_code, false).await
}
pub async fn execute_task_with_timing(&self, description: &str, language: Option<&str>, _auto_execute: bool, show_prompt: bool, show_code: bool, show_timing: bool) -> Result<String> {
info!("Executing task: {}", description);
let total_start = Instant::now();
let provider = self.providers.get(None)?;
let system_prompt = format!(
"You are G3, a code-first AI agent. Your goal is to solve problems by writing and executing code autonomously.
When given a task:
1. Analyze what needs to be done
2. Choose the most appropriate programming language{}
3. Include any necessary imports/dependencies
4. Add error handling where appropriate
5. EXECUTE the code immediately to solve the user's problem
Prefer these languages for different tasks (in order of preference):
- Bash/Shell: File operations, system administration, simple tasks, process management, text processing
- Python: Complex data processing, web scraping, APIs, when libraries are needed
- Rust: Performance-critical tasks, system programming
For simple tasks like listing files, checking processes, basic text manipulation, etc. - prefer bash/shell.
Only use Rust/Python when you need libraries or complex logic that bash can't handle easily.
Format your response as:
```[language]
[code]
```
Then execute it and show the output.",
if let Some(lang) = language {
format!(" (prefer {})", lang)
} else {
" based on the task type".to_string()
}
);
if show_prompt {
println!("🔍 System Prompt:");
println!("================");
println!("{}", system_prompt);
println!("================");
println!();
}
let messages = vec![
Message {
role: MessageRole::System,
content: system_prompt,
},
Message {
role: MessageRole::User,
content: format!("Task: {}", description),
},
];
let request = CompletionRequest {
messages,
max_tokens: Some(2048),
temperature: Some(0.2),
stream: false,
};
// Time the LLM call
let llm_start = Instant::now();
let response = provider.complete(request).await?;
let llm_duration = llm_start.elapsed();
// Time the code execution
let exec_start = Instant::now();
let executor = CodeExecutor::new();
let result = executor.execute_from_response_with_options(&response.content, show_code).await?;
let exec_duration = exec_start.elapsed();
let total_duration = total_start.elapsed();
if show_timing {
let timing_summary = format!(
"\n⏱️ Task Summary:\n LLM call: {}\n Code execution: {}\n Total time: {}",
Self::format_duration(llm_duration),
Self::format_duration(exec_duration),
Self::format_duration(total_duration)
);
Ok(format!("{}\n{}", result, timing_summary))
} else {
Ok(result)
}
}
fn format_duration(duration: Duration) -> String {
let total_ms = duration.as_millis();
if total_ms < 1000 {
format!("{}ms", total_ms)
} else if total_ms < 60_000 {
let seconds = duration.as_secs_f64();
format!("{:.1}s", seconds)
} else {
let minutes = total_ms / 60_000;
let remaining_seconds = (total_ms % 60_000) as f64 / 1000.0;
format!("{}m {:.1}s", minutes, remaining_seconds)
}
}
pub async fn create_automation(&self, workflow: &str) -> Result<String> {
info!("Creating automation for: {}", workflow);
let provider = self.providers.get(None)?;
let messages = vec![
Message {
role: MessageRole::System,
content: "You are G3, a code-first AI agent. Create automation scripts that can be saved and reused. Focus on creating robust, well-documented scripts with error handling and logging.".to_string(),
},
Message {
role: MessageRole::User,
content: format!("Create an automation script for: {}", workflow),
},
];
let request = CompletionRequest {
messages,
max_tokens: Some(2048),
temperature: Some(0.1),
stream: false,
};
let response = provider.complete(request).await?;
Ok(response.content)
}
pub async fn process_data(&self, operation: &str, input_file: Option<&str>) -> Result<String> {
info!("Processing data: {}", operation);
let provider = self.providers.get(None)?;
let context = if let Some(file) = input_file {
format!("Operation: {}\nInput file: {}", operation, file)
} else {
format!("Operation: {}", operation)
};
let messages = vec![
Message {
role: MessageRole::System,
content: "You are G3, a code-first AI agent specializing in data processing. Write Python code using pandas, numpy, or other appropriate libraries to process and analyze data. Always include data validation and error handling.".to_string(),
},
Message {
role: MessageRole::User,
content: context,
},
];
let request = CompletionRequest {
messages,
max_tokens: Some(2048),
temperature: Some(0.1),
stream: false,
};
let response = provider.complete(request).await?;
Ok(response.content)
}
pub async fn execute_web_task(&self, task: &str, url: Option<&str>) -> Result<String> {
info!("Executing web task: {}", task);
let provider = self.providers.get(None)?;
let context = if let Some(url) = url {
format!("Task: {}\nURL: {}", task, url)
} else {
format!("Task: {}", task)
};
let messages = vec![
Message {
role: MessageRole::System,
content: "You are G3, a code-first AI agent for web tasks. Write code for web scraping, API calls, downloads, or web automation. Use appropriate libraries like requests, BeautifulSoup, selenium, or similar. Always respect robots.txt and rate limits.".to_string(),
},
Message {
role: MessageRole::User,
content: context,
},
];
let request = CompletionRequest {
messages,
max_tokens: Some(2048),
temperature: Some(0.2),
stream: false,
};
let response = provider.complete(request).await?;
Ok(response.content)
}
pub async fn execute_file_operation(&self, operation: &str, path: Option<&str>) -> Result<String> {
info!("Executing file operation: {}", operation);
let provider = self.providers.get(None)?;
let context = if let Some(path) = path {
format!("Operation: {}\nPath: {}", operation, path)
} else {
format!("Operation: {}", operation)
};
let messages = vec![
Message {
role: MessageRole::System,
content: "You are G3, a code-first AI agent for file operations. Write scripts (bash, Python, or other appropriate languages) for file management, organization, backup, compression, format conversion, etc. Always include safety checks and confirmation prompts for destructive operations.".to_string(),
},
Message {
role: MessageRole::User,
content: context,
},
];
let request = CompletionRequest {
messages,
max_tokens: Some(2048),
temperature: Some(0.1),
stream: false,
};
let response = provider.complete(request).await?;
Ok(response.content)
}
fn read_file_or_directory(&self, path: &str) -> Result<String> {
let path = Path::new(path);
if path.is_file() {
Ok(std::fs::read_to_string(path)?)
} else if path.is_dir() {
// For directories, read multiple files and combine them
let mut content = String::new();
self.read_directory_recursive(path, &mut content)?;
Ok(content)
} else {
anyhow::bail!("Path does not exist: {}", path.display())
}
}
fn read_directory_recursive(&self, dir: &Path, content: &mut String) -> Result<()> {
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
if let Some(ext) = path.extension() {
// Only read common code files
if matches!(ext.to_str(), Some("rs" | "py" | "js" | "ts" | "go" | "java" | "cpp" | "c" | "h")) {
content.push_str(&format!("\n--- {} ---\n", path.display()));
if let Ok(file_content) = std::fs::read_to_string(&path) {
content.push_str(&file_content);
}
}
}
} else if path.is_dir() && !path.file_name().unwrap().to_str().unwrap().starts_with('.') {
self.read_directory_recursive(&path, content)?;
}
}
Ok(())
}
}
impl std::fmt::Display for AnalysisResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Code Analysis Results")?;
writeln!(f, "====================")?;
writeln!(f)?;
writeln!(f, "Summary:")?;
writeln!(f, "{}", self.summary)?;
writeln!(f)?;
writeln!(f, "Metrics:")?;
writeln!(f, "- Lines of Code: {}", self.metrics.lines_of_code)?;
writeln!(f, "- Complexity Score: {:.2}", self.metrics.complexity_score)?;
writeln!(f, "- Maintainability Index: {:.2}", self.metrics.maintainability_index)?;
Ok(())
}
}
pub mod providers {
pub mod openai;
pub mod anthropic;
}

View File

@@ -0,0 +1,170 @@
use g3_providers::{LLMProvider, CompletionRequest, CompletionResponse, CompletionStream, CompletionChunk, Usage, Message, MessageRole};
use anyhow::Result;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use tracing::{debug, error};
use tokio::sync::mpsc;
use tokio_stream::wrappers::ReceiverStream;
pub struct AnthropicProvider {
client: Client,
api_key: String,
model: String,
}
#[derive(Debug, Serialize)]
struct AnthropicRequest {
model: String,
messages: Vec<AnthropicMessage>,
#[serde(skip_serializing_if = "Option::is_none")]
system: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
max_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
temperature: Option<f32>,
}
#[derive(Debug, Serialize)]
struct AnthropicMessage {
role: String,
content: String,
}
#[derive(Debug, Deserialize)]
struct AnthropicResponse {
content: Vec<AnthropicContent>,
usage: AnthropicUsage,
model: String,
}
#[derive(Debug, Deserialize)]
struct AnthropicContent {
#[serde(rename = "type")]
content_type: String,
text: String,
}
#[derive(Debug, Deserialize)]
struct AnthropicUsage {
input_tokens: u32,
output_tokens: u32,
}
impl AnthropicProvider {
pub fn new(api_key: String, model: String) -> Result<Self> {
let client = Client::new();
Ok(Self {
client,
api_key,
model,
})
}
fn convert_message(&self, message: &Message) -> AnthropicMessage {
AnthropicMessage {
role: match message.role {
MessageRole::System => "system".to_string(),
MessageRole::User => "user".to_string(),
MessageRole::Assistant => "assistant".to_string(),
},
content: message.content.clone(),
}
}
}
#[async_trait::async_trait]
impl LLMProvider for AnthropicProvider {
async fn complete(&self, request: CompletionRequest) -> Result<CompletionResponse> {
debug!("Making Anthropic completion request");
// Separate system messages from other messages
let mut system_content: Option<String> = None;
let mut non_system_messages = Vec::new();
for message in &request.messages {
match message.role {
MessageRole::System => {
// Combine multiple system messages if present
if let Some(existing) = &system_content {
system_content = Some(format!("{}\n\n{}", existing, message.content));
} else {
system_content = Some(message.content.clone());
}
}
_ => {
non_system_messages.push(self.convert_message(message));
}
}
}
let anthropic_request = AnthropicRequest {
model: self.model.clone(),
system: system_content,
messages: non_system_messages,
max_tokens: request.max_tokens,
temperature: request.temperature,
};
let response = self
.client
.post("https://api.anthropic.com/v1/messages")
.header("x-api-key", &self.api_key)
.header("Content-Type", "application/json")
.header("anthropic-version", "2023-06-01")
.json(&anthropic_request)
.send()
.await?;
if !response.status().is_success() {
let error_text = response.text().await?;
error!("Anthropic API error: {}", error_text);
anyhow::bail!("Anthropic API error: {}", error_text);
}
let anthropic_response: AnthropicResponse = response.json().await?;
let content = anthropic_response
.content
.first()
.map(|content| content.text.clone())
.unwrap_or_default();
Ok(CompletionResponse {
content,
usage: Usage {
prompt_tokens: anthropic_response.usage.input_tokens,
completion_tokens: anthropic_response.usage.output_tokens,
total_tokens: anthropic_response.usage.input_tokens + anthropic_response.usage.output_tokens,
},
model: anthropic_response.model,
})
}
async fn stream(&self, request: CompletionRequest) -> Result<CompletionStream> {
debug!("Making Anthropic streaming request");
let (tx, rx) = mpsc::channel(100);
// For now, just send the complete response as a single chunk
// In a real implementation, we'd handle Server-Sent Events
let completion = self.complete(request).await?;
let chunk = CompletionChunk {
content: completion.content,
finished: true,
};
tx.send(Ok(chunk)).await.map_err(|_| anyhow::anyhow!("Failed to send chunk"))?;
Ok(ReceiverStream::new(rx))
}
fn name(&self) -> &str {
"anthropic"
}
fn model(&self) -> &str {
&self.model
}
}

View File

@@ -0,0 +1,2 @@
pub mod openai;
pub mod anthropic;

View File

@@ -0,0 +1,157 @@
use g3_providers::{LLMProvider, CompletionRequest, CompletionResponse, CompletionStream, CompletionChunk, Usage, Message, MessageRole};
use anyhow::Result;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use tracing::{debug, error};
use tokio::sync::mpsc;
use tokio_stream::wrappers::ReceiverStream;
pub struct OpenAIProvider {
client: Client,
api_key: String,
model: String,
base_url: String,
}
#[derive(Debug, Serialize)]
struct OpenAIRequest {
model: String,
messages: Vec<OpenAIMessage>,
#[serde(skip_serializing_if = "Option::is_none")]
max_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
temperature: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
stream: Option<bool>,
}
#[derive(Debug, Serialize)]
struct OpenAIMessage {
role: String,
content: String,
}
#[derive(Debug, Deserialize)]
struct OpenAIResponse {
choices: Vec<OpenAIChoice>,
usage: OpenAIUsage,
model: String,
}
#[derive(Debug, Deserialize)]
struct OpenAIChoice {
message: OpenAIResponseMessage,
finish_reason: Option<String>,
}
#[derive(Debug, Deserialize)]
struct OpenAIResponseMessage {
content: String,
}
#[derive(Debug, Deserialize)]
struct OpenAIUsage {
prompt_tokens: u32,
completion_tokens: u32,
total_tokens: u32,
}
impl OpenAIProvider {
pub fn new(api_key: String, model: String, base_url: Option<String>) -> Result<Self> {
let client = Client::new();
let base_url = base_url.unwrap_or_else(|| "https://api.openai.com/v1".to_string());
Ok(Self {
client,
api_key,
model,
base_url,
})
}
fn convert_message(&self, message: &Message) -> OpenAIMessage {
OpenAIMessage {
role: match message.role {
MessageRole::System => "system".to_string(),
MessageRole::User => "user".to_string(),
MessageRole::Assistant => "assistant".to_string(),
},
content: message.content.clone(),
}
}
}
#[async_trait::async_trait]
impl LLMProvider for OpenAIProvider {
async fn complete(&self, request: CompletionRequest) -> Result<CompletionResponse> {
debug!("Making OpenAI completion request");
let openai_request = OpenAIRequest {
model: self.model.clone(),
messages: request.messages.iter().map(|m| self.convert_message(m)).collect(),
max_tokens: request.max_tokens,
temperature: request.temperature,
stream: Some(false),
};
let response = self
.client
.post(&format!("{}/chat/completions", self.base_url))
.header("Authorization", format!("Bearer {}", self.api_key))
.header("Content-Type", "application/json")
.json(&openai_request)
.send()
.await?;
if !response.status().is_success() {
let error_text = response.text().await?;
error!("OpenAI API error: {}", error_text);
anyhow::bail!("OpenAI API error: {}", error_text);
}
let openai_response: OpenAIResponse = response.json().await?;
let content = openai_response
.choices
.first()
.map(|choice| choice.message.content.clone())
.unwrap_or_default();
Ok(CompletionResponse {
content,
usage: Usage {
prompt_tokens: openai_response.usage.prompt_tokens,
completion_tokens: openai_response.usage.completion_tokens,
total_tokens: openai_response.usage.total_tokens,
},
model: openai_response.model,
})
}
async fn stream(&self, request: CompletionRequest) -> Result<CompletionStream> {
debug!("Making OpenAI streaming request");
let (tx, rx) = mpsc::channel(100);
// For now, just send the complete response as a single chunk
// In a real implementation, we'd handle Server-Sent Events
let completion = self.complete(request).await?;
let chunk = CompletionChunk {
content: completion.content,
finished: true,
};
tx.send(Ok(chunk)).await.map_err(|_| anyhow::anyhow!("Failed to send chunk"))?;
Ok(ReceiverStream::new(rx))
}
fn name(&self) -> &str {
"openai"
}
fn model(&self) -> &str {
&self.model
}
}

View File

@@ -0,0 +1,13 @@
[package]
name = "g3-execution"
version = "0.1.0"
edition = "2021"
description = "Code execution engine for G3 AI agent"
[dependencies]
tokio = { workspace = true }
anyhow = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
regex = "1.0"
tempfile = "3.0"

View File

@@ -0,0 +1,179 @@
use anyhow::Result;
use regex::Regex;
use std::process::Command;
use tempfile::NamedTempFile;
use std::io::Write;
use tracing::{info, debug, error};
pub struct CodeExecutor {
// Future: add configuration for execution limits, sandboxing, etc.
}
#[derive(Debug, Clone)]
pub struct ExecutionResult {
pub stdout: String,
pub stderr: String,
pub exit_code: i32,
pub success: bool,
}
impl CodeExecutor {
pub fn new() -> Self {
Self {}
}
/// Extract code blocks from LLM response and execute them
pub async fn execute_from_response(&self, response: &str) -> Result<String> {
self.execute_from_response_with_options(response, true).await
}
/// Extract code blocks from LLM response and execute them with UI options
pub async fn execute_from_response_with_options(&self, response: &str, show_code: bool) -> Result<String> {
let code_blocks = self.extract_code_blocks(response)?;
if code_blocks.is_empty() {
if show_code {
return Ok(format!("⚠️ No executable code blocks found in response.\n\n{}", response));
} else {
return Ok("⚠️ No executable code found.".to_string());
}
}
let mut results = Vec::new();
// Only show the original LLM response if show_code is true
if show_code {
results.push(response.to_string());
results.push("\n🚀 Executing code...\n".to_string());
}
for (language, code) in code_blocks {
info!("Executing {} code", language);
if show_code {
results.push(format!("📋 Running {} code:", language));
}
match self.execute_code(&language, &code).await {
Ok(result) => {
if result.success {
if show_code {
results.push("✅ Success".to_string());
}
// Always show stdout if there is any, regardless of show_code
if !result.stdout.is_empty() {
results.push(result.stdout.trim().to_string());
}
} else {
results.push("❌ Failed".to_string());
if !result.stderr.is_empty() {
results.push(format!("Error: {}", result.stderr.trim()));
}
}
}
Err(e) => {
error!("Failed to execute {} code: {}", language, e);
results.push(format!("❌ Execution failed: {}", e));
}
}
}
// If no results were added (e.g., successful execution with no output),
// return a simple success message when show_code is false
if results.is_empty() && !show_code {
Ok("✅ Done".to_string())
} else {
Ok(results.join("\n"))
}
}
/// Extract code blocks from markdown-formatted text
fn extract_code_blocks(&self, text: &str) -> Result<Vec<(String, String)>> {
let re = Regex::new(r"```(\w+)?\n(.*?)```")?;
let mut blocks = Vec::new();
for cap in re.captures_iter(text) {
let language = cap.get(1)
.map(|m| m.as_str().to_lowercase())
.unwrap_or_else(|| "bash".to_string()); // Default to bash
let code = cap.get(2).map(|m| m.as_str()).unwrap_or("").trim();
if !code.is_empty() {
blocks.push((language, code.to_string()));
}
}
Ok(blocks)
}
/// Execute code in the specified language
async fn execute_code(&self, language: &str, code: &str) -> Result<ExecutionResult> {
match language.to_lowercase().as_str() {
"python" | "py" => self.execute_python(code).await,
"bash" | "shell" | "sh" => self.execute_bash(code).await,
"javascript" | "js" => self.execute_javascript(code).await,
_ => {
// Try to execute as bash by default
debug!("Unknown language '{}', trying as bash", language);
self.execute_bash(code).await
}
}
}
/// Execute Python code
async fn execute_python(&self, code: &str) -> Result<ExecutionResult> {
let mut temp_file = NamedTempFile::new()?;
temp_file.write_all(code.as_bytes())?;
let temp_path = temp_file.path();
let output = Command::new("python3")
.arg(temp_path)
.output()?;
Ok(ExecutionResult {
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
exit_code: output.status.code().unwrap_or(-1),
success: output.status.success(),
})
}
/// Execute Bash code
async fn execute_bash(&self, code: &str) -> Result<ExecutionResult> {
let output = Command::new("bash")
.arg("-c")
.arg(code)
.output()?;
Ok(ExecutionResult {
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
exit_code: output.status.code().unwrap_or(-1),
success: output.status.success(),
})
}
/// Execute JavaScript code (requires Node.js)
async fn execute_javascript(&self, code: &str) -> Result<ExecutionResult> {
let mut temp_file = NamedTempFile::new()?;
temp_file.write_all(code.as_bytes())?;
let temp_path = temp_file.path();
let output = Command::new("node")
.arg(temp_path)
.output()?;
Ok(ExecutionResult {
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
exit_code: output.status.code().unwrap_or(-1),
success: output.status.success(),
})
}
}
impl Default for CodeExecutor {
fn default() -> Self {
Self::new()
}
}

View File

@@ -0,0 +1,16 @@
[package]
name = "g3-providers"
version = "0.1.0"
edition = "2021"
description = "LLM provider abstractions for G3 AI coding agent"
[dependencies]
tokio = { workspace = true }
reqwest = { workspace = true }
anyhow = { workspace = true }
thiserror = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tracing = { workspace = true }
async-trait = "0.1"
tokio-stream = "0.1"

View File

@@ -0,0 +1,113 @@
use serde::{Deserialize, Serialize};
use anyhow::Result;
use std::collections::HashMap;
/// Trait for LLM providers
#[async_trait::async_trait]
pub trait LLMProvider: Send + Sync {
/// Generate a completion for the given messages
async fn complete(&self, request: CompletionRequest) -> Result<CompletionResponse>;
/// Stream a completion for the given messages
async fn stream(&self, request: CompletionRequest) -> Result<CompletionStream>;
/// Get the provider name
fn name(&self) -> &str;
/// Get the model name
fn model(&self) -> &str;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompletionRequest {
pub messages: Vec<Message>,
pub max_tokens: Option<u32>,
pub temperature: Option<f32>,
pub stream: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message {
pub role: MessageRole,
pub content: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MessageRole {
System,
User,
Assistant,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompletionResponse {
pub content: String,
pub usage: Usage,
pub model: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Usage {
pub prompt_tokens: u32,
pub completion_tokens: u32,
pub total_tokens: u32,
}
pub type CompletionStream = tokio_stream::wrappers::ReceiverStream<Result<CompletionChunk>>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompletionChunk {
pub content: String,
pub finished: bool,
}
/// Provider registry for managing multiple LLM providers
pub struct ProviderRegistry {
providers: HashMap<String, Box<dyn LLMProvider>>,
default_provider: String,
}
impl ProviderRegistry {
pub fn new() -> Self {
Self {
providers: HashMap::new(),
default_provider: String::new(),
}
}
pub fn register<P: LLMProvider + 'static>(&mut self, provider: P) {
let name = provider.name().to_string();
self.providers.insert(name.clone(), Box::new(provider));
if self.default_provider.is_empty() {
self.default_provider = name;
}
}
pub fn set_default(&mut self, provider_name: &str) -> Result<()> {
if !self.providers.contains_key(provider_name) {
anyhow::bail!("Provider '{}' not found", provider_name);
}
self.default_provider = provider_name.to_string();
Ok(())
}
pub fn get(&self, provider_name: Option<&str>) -> Result<&dyn LLMProvider> {
let name = provider_name.unwrap_or(&self.default_provider);
self.providers
.get(name)
.map(|p| p.as_ref())
.ok_or_else(|| anyhow::anyhow!("Provider '{}' not found", name))
}
pub fn list_providers(&self) -> Vec<&str> {
self.providers.keys().map(|s| s.as_str()).collect()
}
}
impl Default for ProviderRegistry {
fn default() -> Self {
Self::new()
}
}

6
src/main.rs Normal file
View File

@@ -0,0 +1,6 @@
use g3_cli::run;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
run().await
}