Skip to main content

sova_ai/
lib.rs

1//! AISDK language models for Sova — `app.install(Ai::…)` / `req.ai()`.
2//!
3//! Thin shell over [`aisdk`](https://crates.io/crates/aisdk): install a default
4//! model into app state, call it from handlers, or drop down to the full
5//! `LanguageModelRequest` builder. [`FakeAi`] records prompts for tests.
6//!
7//! ```ignore
8//! use sova_ai::{Ai, AiExt, FakeAi};
9//!
10//! let fake = FakeAi::new().stub_text("pong");
11//! app.install(Ai::fake(fake.clone()));
12//!
13//! // in a handler:
14//! let out = req.ai().prompt("ping").generate().await?;
15//! assert_eq!(out.text().as_deref(), Some("pong"));
16//! ```
17
18mod bound;
19mod client;
20mod error;
21mod fake;
22mod model;
23mod stream;
24
25pub use bound::{AiBound, AiExt};
26pub use client::{Ai, AiClient};
27pub use error::AiError;
28pub use fake::FakeAi;
29pub use model::SharedModel;
30pub use stream::stream_to_response;
31
32/// Re-export the upstream SDK so apps can use providers / tools without a second dep.
33pub use aisdk;
34
35/// Common AISDK imports for handlers and agents.
36pub mod prelude {
37    pub use aisdk::core::{
38        utils::step_count_is, GenerateTextResponse, LanguageModel, LanguageModelRequest,
39        LanguageModelStreamChunkType, Message, Messages, Tool,
40    };
41    pub use aisdk::macros::tool;
42    pub use aisdk::{Error as AisdkError, Result as AisdkResult};
43
44    pub use crate::{Ai, AiBound, AiClient, AiError, AiExt, FakeAi, SharedModel};
45}
46
47use sova_core::{App, Plugin};
48
49impl Plugin for Ai {
50    fn id(&self) -> &'static str {
51        "ai"
52    }
53
54    fn meta(&self) -> sova_core::PluginMeta {
55        sova_core::PluginMeta::new("Ai")
56            .description("AISDK language models (chat, tools, stream, fake)")
57            .version(env!("CARGO_PKG_VERSION"))
58    }
59
60    fn install(self, app: &mut App) {
61        let mut ai = self;
62        if ai.default_system().is_none() {
63            if let Some(doc) = app.config_doc() {
64                if let Some(section) = doc.section("ai") {
65                    if let Some(system) = section.get("system").and_then(|v| v.as_str()) {
66                        ai = ai.system(system);
67                    }
68                }
69            }
70        }
71        let client = match ai.into_client() {
72            Ok(c) => c,
73            Err(err) => {
74                tracing::error!(error = %err, "ai plugin install failed");
75                panic!("ai install failed: {err}");
76            }
77        };
78        app.state(client);
79    }
80}