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
|
||||
!config.yaml.example
|
||||
!src/
|
||||
!src/*/
|
||||
!src/*.rs
|
||||
!src/**/*.rs
|
||||
!*.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