1use async_trait::async_trait;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use thiserror::Error;
7
8use crate::message::ChatMessage;
9use crate::types::{LLMChunk, LLMConfig, LLMFeature, LLMResponse, LLMToolRequest, ToolChoice};
10
11#[async_trait]
17pub trait LLMProvider: Send + Sync {
18 async fn complete(
20 &self,
21 messages: &[ChatMessage],
22 config: Option<&LLMConfig>,
23 ) -> Result<LLMResponse, LLMError>;
24
25 async fn complete_with_tools(
27 &self,
28 _messages: &[ChatMessage],
29 _config: Option<&LLMConfig>,
30 _request: &LLMToolRequest,
31 ) -> Result<LLMResponse, LLMError> {
32 Err(LLMError::Other(format!(
33 "provider '{}' does not support native tool completion",
34 self.provider_name()
35 )))
36 }
37
38 fn configured_tool_choice(&self) -> Option<ToolChoice> {
40 None
41 }
42
43 fn supports_tool_choice(&self, _choice: &ToolChoice) -> bool {
45 false
46 }
47
48 async fn complete_stream(
50 &self,
51 messages: &[ChatMessage],
52 config: Option<&LLMConfig>,
53 ) -> Result<Box<dyn futures::Stream<Item = Result<LLMChunk, LLMError>> + Unpin + Send>, LLMError>;
54
55 fn provider_name(&self) -> &str;
57
58 fn supports(&self, feature: LLMFeature) -> bool;
60}
61
62#[async_trait]
64pub trait LLMCapability: Send + Sync {
65 async fn select_tool(
66 &self,
67 context: &TaskContext,
68 user_input: &str,
69 ) -> Result<ToolSelection, LLMError>;
70
71 async fn generate_tool_args(
72 &self,
73 tool_id: &str,
74 user_input: &str,
75 schema: &serde_json::Value,
76 ) -> Result<serde_json::Value, LLMError>;
77
78 async fn evaluate_yesno(
79 &self,
80 question: &str,
81 context: &TaskContext,
82 ) -> Result<(bool, String), LLMError>;
83
84 async fn classify(&self, input: &str, categories: &[String])
85 -> Result<(String, f32), LLMError>;
86
87 async fn process_task(
88 &self,
89 context: &TaskContext,
90 system_prompt: &str,
91 ) -> Result<LLMResponse, LLMError>;
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct TaskContext {
97 pub current_state: Option<String>,
98 pub available_tools: Vec<String>,
99 pub memory_slots: HashMap<String, serde_json::Value>,
100 pub recent_messages: Vec<ChatMessage>,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct ToolSelection {
106 pub tool_id: String,
107 pub confidence: f32,
108 pub reasoning: Option<String>,
109}
110
111#[derive(Debug, Error)]
113pub enum LLMError {
114 #[error("API error: {message}")]
115 API {
116 message: String,
117 status: Option<u16>,
118 },
119
120 #[error("Network error: {0}")]
121 Network(String),
122
123 #[error("Rate limit exceeded: {retry_after:?}")]
124 RateLimit {
125 retry_after: Option<std::time::Duration>,
126 },
127
128 #[error("Configuration error: {0}")]
129 Config(String),
130
131 #[error("Model not found: {0}")]
132 ModelNotFound(String),
133
134 #[error("Content filtered: {0}")]
135 ContentFiltered(String),
136
137 #[error("Serialization error: {0}")]
138 Serialization(String),
139
140 #[error("Other error: {0}")]
141 Other(String),
142}
143
144impl From<serde_json::Error> for LLMError {
145 fn from(err: serde_json::Error) -> Self {
146 LLMError::Serialization(err.to_string())
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use crate::types::FinishReason;
154
155 struct LegacyProvider;
156
157 #[async_trait]
158 impl LLMProvider for LegacyProvider {
159 async fn complete(
160 &self,
161 _messages: &[ChatMessage],
162 _config: Option<&LLMConfig>,
163 ) -> Result<LLMResponse, LLMError> {
164 Ok(LLMResponse::new("legacy", FinishReason::Stop))
165 }
166
167 async fn complete_stream(
168 &self,
169 _messages: &[ChatMessage],
170 _config: Option<&LLMConfig>,
171 ) -> Result<
172 Box<dyn futures::Stream<Item = Result<LLMChunk, LLMError>> + Unpin + Send>,
173 LLMError,
174 > {
175 Ok(Box::new(futures::stream::empty()))
176 }
177
178 fn provider_name(&self) -> &str {
179 "legacy"
180 }
181
182 fn supports(&self, _feature: LLMFeature) -> bool {
183 false
184 }
185 }
186
187 #[test]
188 fn additive_tool_methods_preserve_legacy_implementations() {
189 let provider = LegacyProvider;
190 let request = LLMToolRequest {
191 tools: Vec::new(),
192 choice: ToolChoice::Auto,
193 };
194
195 assert!(provider.configured_tool_choice().is_none());
196 assert!(!provider.supports_tool_choice(&ToolChoice::Auto));
197 let error = futures::executor::block_on(provider.complete_with_tools(&[], None, &request))
198 .unwrap_err();
199 assert!(
200 error
201 .to_string()
202 .contains("does not support native tool completion")
203 );
204 }
205}