Skip to main content

sakura_ai_rs/
lib.rs

1//! Sakura-AI-rs
2//!
3//! ### [さくらのAI Engine](https://www.sakura.ad.jp/aipf/ai-engine/) を簡単に使うための非公式Rustクライアント
4//!
5//! [ollama-rs](https://github.com/pepperoni21/ollama-rs)と互換性を持つことで既存のコードを最小限の変更で再利用できるようにしています。
6//!
7//! ※現在はChat Completionのみをサポート
8//!
9//! ## インストール
10//!
11//! ollama-rsとsakura-ai-rsをCargo.tomlのdependenciesに追加
12//!
13//!```bash
14//!cargo add sakura-ai-rs
15//!cargo add ollama-rs
16//!```
17//!
18//! ## 使い方
19//!
20//! [アカウントトークン](https://secure.sakura.ad.jp/ai/account-tokens)を作成し、環境変数`SAKURA_AI_ENGINE_API_KEY`に設定します。
21//!
22//!```bash
23//!# macOS / Linux
24//!export SAKURA_AI_ENGINE_API_KEY='...'
25//!```
26//!
27//!```powershell
28//!# Windows PowerShell
29//!$env:SAKURA_AI_ENGINE_API_KEY = '...'
30//!```
31//!
32//! ### SakuraAIの初期化
33//!
34//!```rust
35//!use sakura_ai_rs::SakuraAI;
36//!
37//!let sakura = SakuraAI::default();
38//!```
39//!
40//! ### Chat Completion API
41//!
42//! `send_chat_messages`を用いてSakura AI Engineの[Chat Completion](https://manual.sakura.ad.jp/api/cloud/ai-engine/inference.html#operation/createChatCompletion)を呼び出す。
43//!
44//!```rust
45//!# tokio_test::block_on(async {
46//!use ollama_rs::generation::chat::{ChatMessage, request::ChatMessageRequest};
47//!use ollama_rs::history::ChatHistory;
48//!use sakura_ai_rs::SakuraAI;
49//!
50//!let sakura = SakuraAI::default();
51//!
52//!let model = "gpt-oss-120b".to_string();
53//!let prompt = "Why is the sky blue?".to_string();
54//!
55//!let res = sakura
56//!    .send_chat_messages(
57//!        ChatMessageRequest::new(
58//!            model,
59//!            vec![ChatMessage::user(prompt)],
60//!        ),
61//!    )
62//!    .await;
63//!
64//!if let Ok(res) = res {
65//!    println!("{}", res.message.content);
66//!}
67//!# });
68//!```
69//!
70//! `send_chat_messages_stream`の使用例は[examples/chat_api_chatbot.rs](https://github.com/stn/sakura-ai-rs/blob/main/examples/chat_api_chatbot.rs)を参考に。
71//!
72//!```bash
73//!> cargo run --example chat_api_chatbot        
74//!    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.19s
75//!     Running `target\debug\examples\chat_api_chatbot.exe`
76//!
77//!> こんにちは
78//!こんにちは!今日はどのようなお手伝いが必要ですか?
79//!>
80//!```
81//!
82//! ### Chat Completion API with History
83//!
84//! `send_chat_messages_with_history`を用いると会話履歴も管理される。
85//!
86//!```rust
87//!# tokio_test::block_on(async {
88//!use ollama_rs::generation::chat::{ChatMessage, request::ChatMessageRequest};
89//!use ollama_rs::history::ChatHistory;
90//!use sakura_ai_rs::SakuraAI;
91//!
92//!let sakura = SakuraAI::default();
93//!
94//!let model = "llama2:latest".to_string();
95//!let prompt = "Why is the sky blue?".to_string();
96//!// `Vec<ChatMessage>` implements `ChatHistory`,
97//!// but you could also implement it yourself on a custom type
98//!let mut history = vec![];
99//!
100//!let res = sakura
101//!    .send_chat_messages_with_history(
102//!        &mut history, // <- messages will be saved here
103//!        ChatMessageRequest::new(
104//!            model,
105//!            vec![ChatMessage::user(prompt)], // <- You should provide only one message
106//!        ),
107//!    )
108//!    .await;
109//!
110//!if let Ok(res) = res {
111//!    println!("{}", res.message.content);
112//!}
113//!# });
114//!```
115//!
116//! [examples/chat_with_history.rs](https://github.com/stn/sakura-ai-rs/blob/main/examples/chat_with_history.rs)と[examples/chat_with_history_stream.rs](https://github.com/stn/sakura-ai-rs/blob/main/examples/chat_with_history_stream.rs)を参考に。
117//!
118//! ## Ollama-rsからの移行
119//!
120//! 上の例を[ollama-rs](https://github.com/pepperoni21/ollama-rs)と比較すると分かる通り、`ollama_rs::Ollama`の代わりに`sakura_ai_rs::SakuraAI`を使用すればよい。
121//!
122//! 入出力のオブジェクトには同じものを使用しているため、それ以外のコードは同じ。
123//!
124//! 現在、`send_chat_messages`, `send_chat_messages_stream`, `send_chat_messages_with_history`, `send_chat_messages_with_history_stream` がサポートされている。
125//!
126//! ## 公式ドキュメント
127//!
128//! - [さくらのAI Engine](https://manual.sakura.ad.jp/cloud/manual-ai-engine.html)
129//! - [チャット補完・音声文字起こし用API](https://manual.sakura.ad.jp/api/cloud/ai-engine/inference.html)
130//! - [ドキュメント・RAGAPI](https://manual.sakura.ad.jp/api/cloud/ai-engine/rag.html)
131//!
132#![cfg_attr(docsrs, feature(doc_cfg))]
133
134use ollama_rs::IntoUrl;
135use secrecy::SecretString;
136use url::Url;
137
138pub mod generation;
139
140#[derive(Debug, Clone)]
141pub struct SakuraAI {
142    pub(crate) url: Url,
143    pub(crate) reqwest_client: reqwest::Client,
144    #[cfg(feature = "headers")]
145    pub(crate) request_headers: reqwest::header::HeaderMap,
146    pub(crate) api_key: SecretString,
147}
148
149/// The main struct representing an Sakura AI Engine client.
150///
151/// This struct is used to interact with the Sakura AI Engine service.
152///
153/// # Fields
154///
155/// * `url` - The base URL of the Sakura AI Engine service.
156/// * `reqwest_client` - The HTTP client used for requests.
157/// * `request_headers` - Optional headers for requests (enabled with the `headers` feature).
158impl SakuraAI {
159    /// Creates a new `SakuraAI` instance with the specified host and port.
160    ///
161    /// # Arguments
162    ///
163    /// * `host` - The host of the Sakura AI Engine service.
164    /// * `port` - The port of the Sakura AI Engine service.
165    ///
166    /// # Returns
167    ///
168    /// A new `SakuraAI` instance.
169    ///
170    /// # Panics
171    ///
172    /// Panics if the host is not a valid URL or if the URL cannot have a port.
173    pub fn new(host: impl IntoUrl, port: u16) -> Self {
174        let mut url: Url = host.into_url().unwrap();
175        url.set_port(Some(port)).unwrap();
176
177        Self::from_url(url)
178    }
179
180    /// Creates a new `SakuraAI` instance with the specified host, port, and `reqwest` client.
181    ///
182    /// # Arguments
183    ///
184    /// * `host` - The host of the Sakura AI Engine service.
185    /// * `port` - The port of the Sakura AI Engine service.
186    /// * `reqwest_client` - The `reqwest` client instance.
187    ///
188    /// # Returns
189    ///
190    /// A new `SakuraAI` instance with the specified `reqwest` client.
191    ///
192    /// # Panics
193    ///
194    /// Panics if the host is not a valid URL or if the URL cannot have a port.
195    pub fn new_with_client(host: impl IntoUrl, port: u16, reqwest_client: reqwest::Client) -> Self {
196        let mut url: Url = host.into_url().unwrap();
197        url.set_port(Some(port)).unwrap();
198
199        Self {
200            url,
201            reqwest_client,
202            ..Default::default()
203        }
204    }
205
206    /// To use a different API key different from default SAKURA_AI_ENGINE_API_KEY env var
207    pub fn with_api_key<S: Into<String>>(mut self, api_key: S) -> Self {
208        self.api_key = SecretString::from(api_key.into());
209        self
210    }
211
212    /// Attempts to create a new `SakuraAI` instance from a URL.
213    ///
214    /// # Arguments
215    ///
216    /// * `url` - The URL of the Sakura AI Engine service.
217    ///
218    /// # Returns
219    ///
220    /// A `Result` containing the new `SakuraAI` instance or a `url::ParseError`.
221    #[inline]
222    pub fn try_new(url: impl IntoUrl) -> Result<Self, url::ParseError> {
223        Ok(Self::from_url(url.into_url()?))
224    }
225
226    /// Create new instance from a [`Url`].
227    #[inline]
228    pub fn from_url(url: Url) -> Self {
229        Self {
230            url,
231            ..Default::default()
232        }
233    }
234
235    /// Returns the URI of the Sakura AI Engine service as a `String`.
236    ///
237    /// # Panics
238    ///
239    /// Panics if the URL does not have a host.
240    #[inline]
241    pub fn uri(&self) -> String {
242        self.url.host().unwrap().to_string()
243    }
244
245    /// Returns a reference to the URL of the Sakura AI Engine service.
246    pub fn url(&self) -> &Url {
247        &self.url
248    }
249
250    /// Returns the URL of the Sakura AI Engine service as a `&str`.
251    ///
252    /// Syntax in pseudo-BNF:
253    ///
254    /// ```bnf
255    ///   url = scheme ":" [ hierarchical | non-hierarchical ] [ "?" query ]? [ "#" fragment ]?
256    ///   non-hierarchical = non-hierarchical-path
257    ///   non-hierarchical-path = /* Does not start with "/" */
258    ///   hierarchical = authority? hierarchical-path
259    ///   authority = "//" userinfo? host [ ":" port ]?
260    ///   userinfo = username [ ":" password ]? "@"
261    ///   hierarchical-path = [ "/" path-segment ]+
262    /// ```
263    #[inline]
264    pub fn url_str(&self) -> &str {
265        self.url.as_str()
266    }
267}
268
269impl From<Url> for SakuraAI {
270    fn from(url: Url) -> Self {
271        Self::from_url(url)
272    }
273}
274
275impl Default for SakuraAI {
276    /// Returns a default Sakura instance with the host set to `https://api.ai.sakura.ad.jp`.
277    fn default() -> Self {
278        Self {
279            url: Url::parse("https://api.ai.sakura.ad.jp").unwrap(),
280            reqwest_client: reqwest::Client::new(),
281            #[cfg(feature = "headers")]
282            request_headers: reqwest::header::HeaderMap::new(),
283            api_key: SecretString::from(
284                std::env::var("SAKURA_AI_ENGINE_API_KEY").unwrap_or_default(),
285            ),
286        }
287    }
288}