Skip to content
Merged

Dev #11

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 13 additions & 15 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,22 @@ jobs:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
archive: eullm-agent-linux-x64.tar.gz
binary: eullm-agent-linux-x64
- os: ubuntu-latest
target: aarch64-unknown-linux-gnu
archive: eullm-agent-linux-arm64.tar.gz
binary: eullm-agent-linux-arm64
use_cross: true
- os: macos-latest
target: aarch64-apple-darwin
archive: eullm-agent-macos-arm64.tar.gz
binary: eullm-agent-macos-arm64
# macOS Intel x64 disabled: macos-13 runners have very low availability
# and queue for 15-30+ min. Re-enable when runner availability improves.
# - os: macos-13
# target: x86_64-apple-darwin
# archive: eullm-agent-macos-x64.tar.gz
# binary: eullm-agent-macos-x64
- os: windows-latest
target: x86_64-pc-windows-msvc
archive: eullm-agent-windows-x64.zip
binary: eullm-agent-windows-x64.exe

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -63,25 +63,23 @@ jobs:
- name: Package (Unix)
if: runner.os != 'Windows'
run: |
cp target/${{ matrix.target }}/release/eullm-agent ./eullm-agent
tar czf ${{ matrix.archive }} eullm-agent
sha256sum ${{ matrix.archive }} > ${{ matrix.archive }}.sha256
cp target/${{ matrix.target }}/release/eullm-agent ${{ matrix.binary }}
sha256sum ${{ matrix.binary }} > ${{ matrix.binary }}.sha256

- name: Package (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
Copy-Item "target\${{ matrix.target }}\release\eullm-agent.exe" .
Compress-Archive -Path "eullm-agent.exe" -DestinationPath "${{ matrix.archive }}"
$hash = (Get-FileHash "${{ matrix.archive }}" -Algorithm SHA256).Hash.ToLower()
"$hash ${{ matrix.archive }}" | Out-File -Encoding ascii "${{ matrix.archive }}.sha256"
Copy-Item "target\${{ matrix.target }}\release\eullm-agent.exe" "${{ matrix.binary }}"
$hash = (Get-FileHash "${{ matrix.binary }}" -Algorithm SHA256).Hash.ToLower()
"$hash ${{ matrix.binary }}" | Out-File -Encoding ascii "${{ matrix.binary }}.sha256"

- uses: actions/upload-artifact@v4
with:
name: ${{ matrix.archive }}
name: ${{ matrix.binary }}
path: |
${{ matrix.archive }}
${{ matrix.archive }}.sha256
${{ matrix.binary }}
${{ matrix.binary }}.sha256

- name: Notify Telegram on build failure
if: failure()
Expand Down
7 changes: 4 additions & 3 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::path::PathBuf;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Config {
pub provider: ProviderConfig,
#[serde(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub telegram: Option<TelegramConfig>,
#[serde(default)]
pub tools: ToolsConfig,
Expand All @@ -19,7 +19,7 @@ fn default_max_iterations() -> usize {
20
}

fn default_system_prompt() -> String {
pub fn default_system_prompt() -> String {
"You are EULLM Agent, an autonomous task executor running on EU infrastructure. \
Think step by step. Use the available tools to complete tasks accurately and efficiently. \
When the task is complete, summarise the result clearly."
Expand All @@ -43,6 +43,7 @@ pub enum ProviderConfig {
api_key: String,
#[serde(default = "default_openai_model")]
model: String,
#[serde(skip_serializing_if = "Option::is_none")]
base_url: Option<String>,
},
}
Expand All @@ -60,7 +61,7 @@ fn default_openai_model() -> String {
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct TelegramConfig {
pub token: String,
#[serde(default)]
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub allowed_users: Vec<i64>,
}

Expand Down
10 changes: 8 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ mod config;
mod llm;
mod telegram;
mod tools;
mod wizard;

use config::{Config, ProviderConfig};
use llm::{
Expand Down Expand Up @@ -54,8 +55,13 @@ async fn main() -> Result<()> {
.init();

let cli = Cli::parse();
let config = Config::load(&cli.config)
.with_context(|| format!("Cannot load config from {:?}", cli.config))?;

let config = if cli.config.exists() {
Config::load(&cli.config)
.with_context(|| format!("Cannot load config from {:?}", cli.config))?
} else {
wizard::run(&cli.config)?
};

let llm: Arc<dyn llm::LlmClient> = match &config.provider {
ProviderConfig::Eullm { base_url, model } => {
Expand Down
115 changes: 115 additions & 0 deletions src/wizard.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
use anyhow::Result;
use std::io::{self, Write};

use crate::config::{default_system_prompt, Config, ProviderConfig, TelegramConfig, ToolsConfig};

pub fn run(config_path: &std::path::Path) -> Result<Config> {
println!("\nNo config file found at '{}'.", config_path.display());
println!("Answer a few questions to get started.\n");

let provider = pick_provider()?;
let telegram = pick_telegram()?;

let config = Config {
provider,
telegram,
tools: ToolsConfig::default(),
max_iterations: 20,
system_prompt: default_system_prompt(),
};

let yaml = serde_yaml::to_string(&config)?;
std::fs::write(config_path, &yaml)?;
println!("\nConfig saved to '{}'. Starting...\n", config_path.display());

Ok(config)
}

fn pick_provider() -> Result<ProviderConfig> {
println!("Choose a provider:");
println!(" 1. Anthropic (Claude) [default]");
println!(" 2. EULLM Engine (local)");
println!(" 3. OpenAI / compatible");
println!();

loop {
let choice = prompt("Provider", "1")?;
match choice.trim() {
"1" | "" => return setup_anthropic(),
"2" => return setup_eullm(),
"3" => return setup_openai(),
_ => println!("Enter 1, 2 or 3."),
}
}
}

fn setup_anthropic() -> Result<ProviderConfig> {
println!();
let api_key = prompt_required("Anthropic API key")?;
let model = prompt("Model", "claude-sonnet-4-6")?;
Ok(ProviderConfig::Anthropic { api_key, model })
}

fn setup_eullm() -> Result<ProviderConfig> {
println!();
let base_url = prompt("Server URL", "http://localhost:11434")?;
let model = prompt_required("Model name")?;
Ok(ProviderConfig::Eullm { base_url, model })
}

fn setup_openai() -> Result<ProviderConfig> {
println!();
let api_key = prompt_required("OpenAI API key")?;
let model = prompt("Model", "gpt-4o")?;
let base_url_raw = prompt("Base URL (blank = api.openai.com)", "")?;
Ok(ProviderConfig::OpenAI {
api_key,
model,
base_url: if base_url_raw.is_empty() { None } else { Some(base_url_raw) },
})
}

fn pick_telegram() -> Result<Option<TelegramConfig>> {
println!();
let enable = prompt("Enable Telegram bot? [y/N]", "N")?;
let enable_lower = enable.to_lowercase();
if !matches!(enable_lower.trim(), "y" | "yes") {
return Ok(None);
}
let token = prompt_required("Bot token")?;
let ids_raw = prompt("Allowed user IDs, comma-separated (blank = allow all)", "")?;
let allowed_users = ids_raw
.split(',')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.filter_map(|s| s.parse::<i64>().ok())
.collect();
Ok(Some(TelegramConfig { token, allowed_users }))
}

fn prompt(label: &str, default: &str) -> Result<String> {
if default.is_empty() {
print!("{}: ", label);
} else {
print!("{} [{}]: ", label, default);
}
io::stdout().flush()?;
let mut buf = String::new();
io::stdin().read_line(&mut buf)?;
let val = buf.trim().to_string();
Ok(if val.is_empty() { default.to_string() } else { val })
}

fn prompt_required(label: &str) -> Result<String> {
loop {
print!("{}: ", label);
io::stdout().flush()?;
let mut buf = String::new();
io::stdin().read_line(&mut buf)?;
let val = buf.trim().to_string();
if !val.is_empty() {
return Ok(val);
}
println!(" (required — please enter a value)");
}
}
Loading