foukoapi 0.1.2-alpha.1

Cross-platform bot framework in Rust: one codebase, many platforms. Shared accounts, embeds, keyboards, economy, i18n and pluggable storage; Telegram and Discord adapters included.
Documentation
//! GenAI showcase bot: /ask streams a model's answer into the chat.
//!
//! Uses [`GenClient::chat_stream`] - the reply text grows in place as
//! tokens arrive, edited roughly every two seconds. Defaults target a
//! local Ollama; point the env vars anywhere OpenAI-compatible.
//!
//! ```bash
//! TG_TOKEN=... \
//! GENAI_URL=http://127.0.0.1:11434 GENAI_MODEL=llama3.2 \
//! cargo run --example genai --features "genai telegram"
//! ```

use foukoapi::genai::{ChatMessage, GenClient};
use foukoapi::{Bot, Embed, Platform, Reply, Result};
use std::sync::{Arc, Mutex};

#[tokio::main]
async fn main() -> Result<()> {
    tracing_subscriber::fmt()
        .with_env_filter(
            tracing_subscriber::EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| "info,foukoapi=debug".into()),
        )
        .init();

    // Host config, with defaults for a local Ollama (no key needed).
    let url = std::env::var("GENAI_URL").unwrap_or_else(|_| "http://127.0.0.1:11434".into());
    let key = std::env::var("GENAI_KEY").ok();
    let model = std::env::var("GENAI_MODEL").unwrap_or_else(|_| "llama3.2".into());

    let mut bot = Bot::new();
    if let Ok(token) = std::env::var("TG_TOKEN") {
        bot = bot.add_platform(Platform::telegram(token));
    }

    bot.command("/ask", move |ctx| {
        let (url, key, model) = (url.clone(), key.clone(), model.clone());
        async move {
            let question = ctx.args().trim().to_owned();
            if question.is_empty() {
                return ctx.reply("usage: /ask <question>").await;
            }
            ctx.typing().await;

            // The stream callback is sync, so it only stores the text so
            // far; a background task pushes it to the chat every ~2s.
            let text = Arc::new(Mutex::new(String::new()));
            let editor = {
                let (ctx, text) = (ctx.clone(), text.clone());
                tokio::spawn(async move {
                    let mut shown = String::new();
                    loop {
                        tokio::time::sleep(std::time::Duration::from_secs(2)).await;
                        let now = text.lock().unwrap().clone();
                        if !now.is_empty() && now != shown {
                            let _ = ctx.edit_reply(Reply::text(format!("{now} ..."))).await;
                            shown = now;
                        }
                    }
                })
            };

            let client = GenClient::new(url, key);
            let result = client
                .chat_stream(&model, &[ChatMessage::user(question.as_str())], |so_far| {
                    *text.lock().unwrap() = so_far.to_owned();
                })
                .await;
            editor.abort();

            match result {
                Ok(answer) => {
                    let embed = Embed::new()
                        .title("Answer")
                        .description(answer)
                        .footer(model);
                    ctx.edit_reply(Reply::embed(embed)).await
                }
                Err(e) => ctx.reply(format!("genai error: {e}")).await,
            }
        }
    })
    .run()
    .await
}