Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "eullm-agent"
version = "0.1.0"
version = "0.1.1"
edition = "2021"
license = "Apache-2.0"
description = "EULLM Agent — autonomous ReAct agent with multi-provider LLM support"
Expand Down
5 changes: 5 additions & 0 deletions Cross.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[target.aarch64-unknown-linux-gnu]
pre-build = [
"dpkg --add-architecture arm64",
"apt-get update && apt-get install -y libssl-dev:arm64 pkg-config",
]
82 changes: 36 additions & 46 deletions config.example.yaml
Original file line number Diff line number Diff line change
@@ -1,64 +1,54 @@
# EULLM Agent configuration
# Copy to config.yaml and fill in your values.
# EULLM Agent — example configuration
# Copy this file to config.yaml and fill in your values.

# --- LLM Provider ---
# Choose one: eullm | anthropic | openai
#
# eullm = local EULLM Engine (Ollama-compatible, zero external calls)
# ── LLM Provider ───────────────────────────────────────────────────────────────
# Uncomment ONE provider block.

# Option 1: Anthropic (Claude)
provider:
type: eullm
base_url: "http://localhost:11434"
model: "llama3.2"
type: anthropic
api_key: "sk-ant-..."
model: claude-sonnet-4-6

# Anthropic Claude API
# Option 2: EULLM Engine (local, Ollama-compatible)
# provider:
# type: anthropic
# api_key: "sk-ant-..."
# model: "claude-sonnet-4-6"
# type: eullm
# base_url: http://localhost:11434
# model: qwen3:14b

# OpenAI-compatible: covers OpenAI, Mistral, OpenRouter, LiteLLM proxy
# Use base_url to point at any OpenAI-compatible endpoint.
# Tip: users with a Claude Max subscription can run LiteLLM locally
# (litellm --model claude-3-5-sonnet) and point base_url at it.
# Option 3: OpenAI or any compatible API
# provider:
# type: openai
# api_key: "sk-..."
# model: "gpt-4o"
# base_url: "https://api.openai.com/v1" # optional, default
#
# OpenRouter (pay-as-you-go, 100+ models):
# provider:
# type: openai
# api_key: "sk-or-..."
# model: "anthropic/claude-sonnet-4-6"
# base_url: "https://openrouter.ai/api/v1"
#
# Mistral API (European provider):
# provider:
# type: openai
# api_key: "..."
# model: "mistral-large-latest"
# base_url: "https://api.mistral.ai/v1"
# model: gpt-4o
# # base_url: https://api.openai.com/v1 # change for compatible providers

# --- Telegram Bot ---
# Get a token from @BotFather. allowed_users: leave empty to allow everyone.
# ── Telegram Bot ───────────────────────────────────────────────────────────────
# Required only for `eullm-agent serve`. Get a token from @BotFather.
#
# telegram:
# token: "123456:ABC-DEF..."
# allowed_users: [123456789]
# token: "123456789:ABC-DEFghijklmn..."
# allowed_users: [] # empty = allow anyone; add Telegram user IDs to restrict

# --- Tools ---
# ── Agent Settings ─────────────────────────────────────────────────────────────
max_iterations: 20

system_prompt: |
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.

# ── Tools ──────────────────────────────────────────────────────────────────────
tools:
shell:
enabled: true
allow_sudo: false
allow_sudo: false # set true to permit sudo commands
timeout_seconds: 30

filesystem:
enabled: true
allowed_paths: [] # empty = unrestricted; add paths to sandbox
allowed_paths: [] # empty = no restriction; add absolute paths to sandbox the agent

# --- Agent ---
max_iterations: 20
system_prompt: |
You are EULLM Agent, an autonomous task executor running on EU infrastructure.
Think step by step. Use the available tools to complete tasks accurately.
When done, summarize the result clearly.
http:
enabled: true
timeout_seconds: 30 # per-request timeout
16 changes: 16 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ pub struct ToolsConfig {
pub shell: ShellToolConfig,
#[serde(default)]
pub filesystem: FilesystemToolConfig,
#[serde(default)]
pub http: HttpToolConfig,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
Expand Down Expand Up @@ -102,6 +104,20 @@ impl Default for FilesystemToolConfig {
}
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct HttpToolConfig {
#[serde(default = "bool_true")]
pub enabled: bool,
#[serde(default = "default_timeout")]
pub timeout_seconds: u64,
}

impl Default for HttpToolConfig {
fn default() -> Self {
Self { enabled: true, timeout_seconds: 30 }
}
}

fn bool_true() -> bool {
true
}
Expand Down
5 changes: 5 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use llm::{
};
use tools::{
filesystem::{ListDirTool, ReadFileTool, WriteFileTool},
http::FetchUrlTool,
shell::ShellTool,
ToolRegistry,
};
Expand Down Expand Up @@ -108,5 +109,9 @@ fn build_tool_registry(config: &Config) -> ToolRegistry {
r.register(Box::new(ListDirTool));
}

if tc.http.enabled {
r.register(Box::new(FetchUrlTool::new(tc.http.timeout_seconds)));
}

r
}
120 changes: 120 additions & 0 deletions src/tools/http.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
use anyhow::Result;
use async_trait::async_trait;
use reqwest::Client;
use serde_json::{json, Value};
use std::time::Duration;

use crate::llm::ToolDefinition;
use super::Tool;

pub struct FetchUrlTool {
client: Client,
}

impl FetchUrlTool {
pub fn new(timeout_secs: u64) -> Self {
let client = Client::builder()
.timeout(Duration::from_secs(timeout_secs))
.user_agent("eullm-agent/0.1")
.build()
.expect("Failed to build HTTP client");
Self { client }
}
}

#[async_trait]
impl Tool for FetchUrlTool {
fn definition(&self) -> ToolDefinition {
ToolDefinition {
name: "fetch_url".into(),
description: "Perform an HTTP request (GET/POST/PUT/DELETE) and return the response \
body as text. Use for REST APIs, fetching web content, or any HTTP \
endpoint. JSON responses are returned as-is.".into(),
parameters: json!({
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "The URL to request"
},
"method": {
"type": "string",
"enum": ["GET", "POST", "PUT", "DELETE"],
"description": "HTTP method (default: GET)"
},
"headers": {
"type": "object",
"description": "Additional request headers as key/value pairs",
"additionalProperties": { "type": "string" }
},
"body": {
"type": "string",
"description": "Raw request body (for POST/PUT)"
},
"json_body": {
"description": "Request body sent as JSON; sets Content-Type: application/json automatically"
}
},
"required": ["url"]
}),
}
}

async fn execute(&self, arguments: &Value) -> Result<String> {
let url = arguments["url"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("Missing 'url'"))?;

let method = arguments["method"].as_str().unwrap_or("GET");

let mut req = match method.to_uppercase().as_str() {
"GET" => self.client.get(url),
"POST" => self.client.post(url),
"PUT" => self.client.put(url),
"DELETE" => self.client.delete(url),
m => return Err(anyhow::anyhow!("Unsupported HTTP method: {m}")),
};

if let Some(hdrs) = arguments["headers"].as_object() {
for (k, v) in hdrs {
if let Some(val) = v.as_str() {
req = req.header(k.as_str(), val);
}
}
}

if !arguments["json_body"].is_null() {
req = req.json(&arguments["json_body"]);
} else if let Some(body) = arguments["body"].as_str() {
req = req.body(body.to_string());
}

let resp = req
.send()
.await
.map_err(|e| anyhow::anyhow!("Request failed: {e}"))?;

let status = resp.status();
let text = resp
.text()
.await
.map_err(|e| anyhow::anyhow!("Failed to read response body: {e}"))?;

const MAX_LEN: usize = 12_000;
let body = if text.len() > MAX_LEN {
let mut end = MAX_LEN;
while !text.is_char_boundary(end) {
end -= 1;
}
format!("{}…\n[truncated — {} bytes total]", &text[..end], text.len())
} else {
text
};

if status.is_success() {
Ok(body)
} else {
Ok(format!("[HTTP {status}]\n{body}"))
}
}
}
1 change: 1 addition & 0 deletions src/tools/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use serde_json::Value;
use crate::llm::ToolDefinition;

pub mod filesystem;
pub mod http;
pub mod shell;

#[async_trait]
Expand Down
Loading