From f02ce1f090bcb9f105fb5d7a9dee06bbe3a5af8a Mon Sep 17 00:00:00 2001 From: primoco <58369875+primoco@users.noreply.github.com> Date: Thu, 25 Jun 2026 10:59:20 +0200 Subject: [PATCH] feat: add fetch_url tool, config.example.yaml, Cross.toml; bump to v0.1.1 --- Cargo.toml | 2 +- Cross.toml | 5 ++ config.example.yaml | 82 +++++++++++++----------------- src/config.rs | 16 ++++++ src/main.rs | 5 ++ src/tools/http.rs | 120 ++++++++++++++++++++++++++++++++++++++++++++ src/tools/mod.rs | 1 + 7 files changed, 184 insertions(+), 47 deletions(-) create mode 100644 Cross.toml create mode 100644 src/tools/http.rs diff --git a/Cargo.toml b/Cargo.toml index e008ee9..af68eb1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/Cross.toml b/Cross.toml new file mode 100644 index 0000000..982b45b --- /dev/null +++ b/Cross.toml @@ -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", +] diff --git a/config.example.yaml b/config.example.yaml index ffa6976..3a4e033 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -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 diff --git a/src/config.rs b/src/config.rs index a8d57e3..e693d50 100644 --- a/src/config.rs +++ b/src/config.rs @@ -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)] @@ -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 } diff --git a/src/main.rs b/src/main.rs index 3055103..144e565 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,6 +18,7 @@ use llm::{ }; use tools::{ filesystem::{ListDirTool, ReadFileTool, WriteFileTool}, + http::FetchUrlTool, shell::ShellTool, ToolRegistry, }; @@ -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 } diff --git a/src/tools/http.rs b/src/tools/http.rs new file mode 100644 index 0000000..f0e5857 --- /dev/null +++ b/src/tools/http.rs @@ -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 { + 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}")) + } + } +} diff --git a/src/tools/mod.rs b/src/tools/mod.rs index 001ff3e..fb5f757 100644 --- a/src/tools/mod.rs +++ b/src/tools/mod.rs @@ -5,6 +5,7 @@ use serde_json::Value; use crate::llm::ToolDefinition; pub mod filesystem; +pub mod http; pub mod shell; #[async_trait]