updated
This commit is contained in:
parent
14210d0027
commit
a04df6600b
5 changed files with 375 additions and 0 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -7,6 +7,7 @@
|
||||||
!Cargo.lock
|
!Cargo.lock
|
||||||
!config.yaml.example
|
!config.yaml.example
|
||||||
!src/
|
!src/
|
||||||
|
!src/*/
|
||||||
!src/*.rs
|
!src/*.rs
|
||||||
!src/**/*.rs
|
!src/**/*.rs
|
||||||
!*.md
|
!*.md
|
||||||
|
|
|
||||||
124
src/functions/fetch.rs
Normal file
124
src/functions/fetch.rs
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
pub fn definitions() -> Vec<Value> {
|
||||||
|
vec![serde_json::json!({
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "fetch",
|
||||||
|
"description": "Fetch and summarize the text content of a URL or article link.",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"url": {"type": "string", "description": "The URL to fetch"}
|
||||||
|
},
|
||||||
|
"required": ["url"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(arguments: &Value) -> Value {
|
||||||
|
let url = arguments.get("url").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
|
|
||||||
|
if !url.starts_with("http://") && !url.starts_with("https://") {
|
||||||
|
return serde_json::json!({"error": "Only http/https URLs are supported"});
|
||||||
|
}
|
||||||
|
|
||||||
|
let client = reqwest::Client::builder()
|
||||||
|
.user_agent("Mozilla/5.0")
|
||||||
|
.redirect(reqwest::redirect::Policy::limited(10))
|
||||||
|
.build()
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let resp = match client.get(url).send().await {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => return serde_json::json!({"error": e.to_string()}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let content_type = resp
|
||||||
|
.headers()
|
||||||
|
.get("content-type")
|
||||||
|
.and_then(|v| v.to_str().ok())
|
||||||
|
.unwrap_or("");
|
||||||
|
|
||||||
|
if !content_type.contains("text") && !content_type.contains("json") {
|
||||||
|
return serde_json::json!({"error": format!("Unsupported content type: {content_type}")});
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = match resp.text().await {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(e) => return serde_json::json!({"error": e.to_string()}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let text = strip_html(&text);
|
||||||
|
let truncated: String = text.chars().take(4000).collect();
|
||||||
|
|
||||||
|
serde_json::json!({"url": url, "content": truncated})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn strip_html(input: &str) -> String {
|
||||||
|
let mut result = String::with_capacity(input.len());
|
||||||
|
let mut in_tag = false;
|
||||||
|
let mut in_style = false;
|
||||||
|
let mut in_script = false;
|
||||||
|
|
||||||
|
let lower = input.to_lowercase();
|
||||||
|
let chars: Vec<char> = input.chars().collect();
|
||||||
|
let lower_chars: Vec<char> = lower.chars().collect();
|
||||||
|
let len = chars.len();
|
||||||
|
let mut i = 0;
|
||||||
|
|
||||||
|
while i < len {
|
||||||
|
if i + 7 <= len {
|
||||||
|
let slice: String = lower_chars[i..i + 7].iter().collect();
|
||||||
|
if slice == "<style " || slice == "<style>" {
|
||||||
|
in_style = true;
|
||||||
|
}
|
||||||
|
if slice == "<script" {
|
||||||
|
in_script = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if i + 8 <= len {
|
||||||
|
let slice: String = lower_chars[i..i + 8].iter().collect();
|
||||||
|
if slice == "</style>" {
|
||||||
|
in_style = false;
|
||||||
|
i += 8;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if i + 9 <= len {
|
||||||
|
let slice: String = lower_chars[i..i + 9].iter().collect();
|
||||||
|
if slice == "</script>" {
|
||||||
|
in_script = false;
|
||||||
|
i += 9;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if chars[i] == '<' {
|
||||||
|
in_tag = true;
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if chars[i] == '>' {
|
||||||
|
in_tag = false;
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !in_tag && !in_style && !in_script {
|
||||||
|
if chars[i].is_whitespace() {
|
||||||
|
if !result.ends_with(' ') {
|
||||||
|
result.push(' ');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.push(chars[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.trim().to_string()
|
||||||
|
}
|
||||||
38
src/functions/mod.rs
Normal file
38
src/functions/mod.rs
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
//pub mod discord;
|
||||||
|
pub mod fetch;
|
||||||
|
//pub mod hoyoverse;
|
||||||
|
pub mod search;
|
||||||
|
pub mod weather;
|
||||||
|
use anyhow::Result;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
pub struct ToolContext {
|
||||||
|
//pub http: &'a serenity::Http,
|
||||||
|
pub searxng_url: String,
|
||||||
|
//pub hoyoverse_cookies: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tool_definitions() -> Vec<Value> {
|
||||||
|
let mut defs = Vec::new();
|
||||||
|
defs.extend(search::definitions());
|
||||||
|
defs.extend(weather::definitions());
|
||||||
|
defs.extend(fetch::definitions());
|
||||||
|
//defs.extend(hoyoverse::definitions());
|
||||||
|
defs
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn dispatch(name: &str, arguments: Value, ctx: &ToolContext) -> Result<String> {
|
||||||
|
let result: Value = match name {
|
||||||
|
"search" => search::run(&arguments, ctx).await,
|
||||||
|
"get_weather" => weather::run(&arguments).await,
|
||||||
|
"fetch" => fetch::run(&arguments).await,
|
||||||
|
//"hoyoverse_player_stats" => hoyoverse::player_stats(&arguments, ctx).await,
|
||||||
|
//"hoyoverse_characters" => hoyoverse::characters(&arguments, ctx).await,
|
||||||
|
//"hoyoverse_abyss" => hoyoverse::abyss(&arguments, ctx).await,
|
||||||
|
//"hoyoverse_notes" => hoyoverse::notes(&arguments, ctx).await,
|
||||||
|
//"hoyoverse_daily_checkin" => hoyoverse::daily_checkin(&arguments, ctx).await,
|
||||||
|
_ => serde_json::json!({"error": format!("Unknown tool: {name}")}),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(serde_json::to_string(&result)?)
|
||||||
|
}
|
||||||
71
src/functions/search.rs
Normal file
71
src/functions/search.rs
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
use super::ToolContext;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
pub fn definitions() -> Vec<Value> {
|
||||||
|
vec![serde_json::json!({
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "search",
|
||||||
|
"description": "Search the web using SearXNG for current information, news, or anything the AI doesn't know.",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"query": {"type": "string", "description": "The search query"},
|
||||||
|
"num_results": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of results to return (default 5, max 10)",
|
||||||
|
"default": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["query"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(arguments: &Value, ctx: &ToolContext) -> Value {
|
||||||
|
let query = arguments
|
||||||
|
.get("query")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("");
|
||||||
|
let num_results = arguments
|
||||||
|
.get("num_results")
|
||||||
|
.and_then(|v| v.as_u64())
|
||||||
|
.unwrap_or(5)
|
||||||
|
.min(10) as usize;
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let resp = match client
|
||||||
|
.get(format!("{}/search", ctx.searxng_url))
|
||||||
|
.query(&[("q", query), ("format", "json"), ("categories", "general")])
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => return serde_json::json!({"error": e.to_string()}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let data: Value = match resp.json().await {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(e) => return serde_json::json!({"error": e.to_string()}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let results: Vec<Value> = data
|
||||||
|
.get("results")
|
||||||
|
.and_then(|r| r.as_array())
|
||||||
|
.map(|arr| {
|
||||||
|
arr.iter()
|
||||||
|
.take(num_results)
|
||||||
|
.map(|r| {
|
||||||
|
serde_json::json!({
|
||||||
|
"title": r.get("title").and_then(|v| v.as_str()),
|
||||||
|
"url": r.get("url").and_then(|v| v.as_str()),
|
||||||
|
"snippet": r.get("content").and_then(|v| v.as_str()),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
serde_json::json!({"query": query, "results": results})
|
||||||
|
}
|
||||||
141
src/functions/weather.rs
Normal file
141
src/functions/weather.rs
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
pub fn definitions() -> Vec<Value> {
|
||||||
|
vec![serde_json::json!({
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "get_weather",
|
||||||
|
"description": "Get the current weather for a location using Open-Meteo (no API key required).",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"location": {"type": "string", "description": "City name or location, e.g. 'Tokyo' or 'New York'"}
|
||||||
|
},
|
||||||
|
"required": ["location"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(arguments: &Value) -> Value {
|
||||||
|
let location = arguments
|
||||||
|
.get("location")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("");
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let geo_resp = match client
|
||||||
|
.get("https://geocoding-api.open-meteo.com/v1/search")
|
||||||
|
.query(&[
|
||||||
|
("name", location),
|
||||||
|
("count", "1"),
|
||||||
|
("language", "en"),
|
||||||
|
("format", "json"),
|
||||||
|
])
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => return serde_json::json!({"error": e.to_string()}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let geo_data: Value = match geo_resp.json().await {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(e) => return serde_json::json!({"error": e.to_string()}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(result) = geo_data
|
||||||
|
.get("results")
|
||||||
|
.and_then(|r| r.as_array())
|
||||||
|
.and_then(|a| a.first())
|
||||||
|
else {
|
||||||
|
return serde_json::json!({"error": format!("Location '{location}' not found")});
|
||||||
|
};
|
||||||
|
|
||||||
|
let lat = result
|
||||||
|
.get("latitude")
|
||||||
|
.and_then(|v| v.as_f64())
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
let lon = result
|
||||||
|
.get("longitude")
|
||||||
|
.and_then(|v| v.as_f64())
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
let name = result
|
||||||
|
.get("name")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or(location);
|
||||||
|
let country = result.get("country").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
|
|
||||||
|
let weather_resp = match client
|
||||||
|
.get("https://api.open-meteo.com/v1/forecast")
|
||||||
|
.query(&[
|
||||||
|
("latitude", &lat.to_string()),
|
||||||
|
("longitude", &lon.to_string()),
|
||||||
|
(
|
||||||
|
"current",
|
||||||
|
&"temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m,apparent_temperature".to_string(),
|
||||||
|
),
|
||||||
|
("temperature_unit", &"celsius".to_string()),
|
||||||
|
("wind_speed_unit", &"kmh".to_string()),
|
||||||
|
])
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => return serde_json::json!({"error": e.to_string()}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let weather_data: Value = match weather_resp.json().await {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(e) => return serde_json::json!({"error": e.to_string()}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let current = match weather_data.get("current") {
|
||||||
|
Some(c) => c,
|
||||||
|
None => return serde_json::json!({"error": "No current weather data"}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let weather_code = current
|
||||||
|
.get("weather_code")
|
||||||
|
.and_then(|v| v.as_u64())
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let condition = match weather_code {
|
||||||
|
0 => "Clear sky",
|
||||||
|
1 => "Mainly clear",
|
||||||
|
2 => "Partly cloudy",
|
||||||
|
3 => "Overcast",
|
||||||
|
45 => "Foggy",
|
||||||
|
48 => "Icy fog",
|
||||||
|
51 => "Light drizzle",
|
||||||
|
53 => "Drizzle",
|
||||||
|
55 => "Heavy drizzle",
|
||||||
|
61 => "Light rain",
|
||||||
|
63 => "Rain",
|
||||||
|
65 => "Heavy rain",
|
||||||
|
71 => "Light snow",
|
||||||
|
73 => "Snow",
|
||||||
|
75 => "Heavy snow",
|
||||||
|
80 => "Light showers",
|
||||||
|
81 => "Showers",
|
||||||
|
82 => "Heavy showers",
|
||||||
|
95 => "Thunderstorm",
|
||||||
|
_ => "Unknown",
|
||||||
|
};
|
||||||
|
|
||||||
|
let location_str = if country.is_empty() {
|
||||||
|
name.to_string()
|
||||||
|
} else {
|
||||||
|
format!("{name}, {country}")
|
||||||
|
};
|
||||||
|
|
||||||
|
serde_json::json!({
|
||||||
|
"location": location_str,
|
||||||
|
"temperature_c": current.get("temperature_2m"),
|
||||||
|
"feels_like_c": current.get("apparent_temperature"),
|
||||||
|
"humidity_percent": current.get("relative_humidity_2m"),
|
||||||
|
"wind_speed_kmh": current.get("wind_speed_10m"),
|
||||||
|
"condition": condition,
|
||||||
|
})
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue