// Copyright (c) 2026 Yingjie Shang
// agent-observability is licensed under Mulan PSL v2.
///|
/// Application settings loaded from environment variables or `.env` file.
pub struct Settings {
// LLM provider
provider_name : String
base_url : String
model : String
api_key : String
max_tokens : Int
// Agent behavior
max_tool_turns : Int
// Observability
otel_stdout : Bool
otel_endpoint : String
capture_content : Bool
}
///|
/// Load settings from environment variables or `.env` file.
pub async fn Settings::from_env() -> Settings {
let api_key = env("LLM_API_KEY")
let provider_name = env("LLM_PROVIDER", default="stepfun")
let base_url = env("LLM_BASE_URL", default="https://api.stepfun.com/v1")
let model = env("LLM_MODEL", default="step-3.7-flash")
let max_tokens = parse_int(env("LLM_MAX_TOKENS", default="1024"), 1024)
let max_tool_turns = parse_int(env("AGENT_MAX_TOOL_TURNS", default="10"), 10)
let otel_stdout = env("OTEL_STDOUT").to_lower() == "true"
let otel_endpoint = env(
"OTEL_EXPORTER_OTLP_ENDPOINT",
default="http://localhost:4318",
)
let capture_content = env("CAPTURE_CONTENT").to_lower() == "true"
{
provider_name,
base_url,
model,
api_key,
max_tokens,
max_tool_turns,
otel_stdout,
otel_endpoint,
capture_content,
}
}
///|
/// Read a `.env` file and look up a key.
pub async fn read_dotenv(key : String) -> String? {
let content = (@fs.read_file(".env").text() : String) catch {
_ => return None
}
for line in content.split("\n") {
let trimmed = line.trim(char_set="\r ")
if trimmed.is_empty() || trimmed.has_prefix("#") {
continue
}
match trimmed.split_once("=") {
Some((k, v)) => if k == key { return Some(v.to_owned()) }
None => continue
}
}
None
}
///|
/// Read a configuration value from the process environment or `.env` file.
///
/// Resolution order: process environment > `.env` file > default.
pub async fn env(key : String, default? : String = "") -> String {
match @env.get_env_var(key) {
Some(value) => value
None => read_dotenv(key).unwrap_or(default)
}
}
///|
/// Parse an integer from a string, returning the default on failure.
pub fn parse_int(s : String, default : Int) -> Int {
match s.trim() {
"" => default
text => @string.parse_int(text) catch { _ => default }
}
}