1use std::io::Read;
31
32use fastly::http::{header, Method, StatusCode};
33use fastly::{Backend, Body, Request, Response};
34use cortexai_llm_client::{
35 LlmResponse, Message, Provider, RequestBuilder, ResponseParser, StreamChunk,
36};
37use serde::{Deserialize, Serialize};
38use thiserror::Error;
39
40#[derive(Debug, Error)]
42pub enum FastlyAgentError {
43 #[error("LLM client error: {0}")]
44 LlmClient(#[from] cortexai_llm_client::LlmClientError),
45
46 #[error("Fastly error: {0}")]
47 Fastly(String),
48
49 #[error("HTTP error: {status} - {message}")]
50 Http { status: u16, message: String },
51
52 #[error("JSON error: {0}")]
53 Json(#[from] serde_json::Error),
54
55 #[error("Invalid provider: {0}")]
56 InvalidProvider(String),
57}
58
59impl From<fastly::Error> for FastlyAgentError {
60 fn from(e: fastly::Error) -> Self {
61 FastlyAgentError::Fastly(e.to_string())
62 }
63}
64
65impl From<fastly::http::request::SendError> for FastlyAgentError {
66 fn from(e: fastly::http::request::SendError) -> Self {
67 FastlyAgentError::Fastly(format!("Send error: {:?}", e))
68 }
69}
70
71pub type Result<T> = std::result::Result<T, FastlyAgentError>;
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct FastlyAgentConfig {
76 pub provider: String,
78 pub api_key: String,
80 pub model: String,
82 pub system_prompt: Option<String>,
84 pub temperature: f32,
86 pub max_tokens: u32,
88}
89
90impl FastlyAgentConfig {
91 pub fn new(
93 provider: impl Into<String>,
94 api_key: impl Into<String>,
95 model: impl Into<String>,
96 ) -> Self {
97 Self {
98 provider: provider.into(),
99 api_key: api_key.into(),
100 model: model.into(),
101 system_prompt: None,
102 temperature: 0.7,
103 max_tokens: 4096,
104 }
105 }
106
107 pub fn with_system_prompt(mut self, prompt: impl Into<String>) -> Self {
109 self.system_prompt = Some(prompt.into());
110 self
111 }
112
113 pub fn with_temperature(mut self, temp: f32) -> Self {
115 self.temperature = temp.clamp(0.0, 2.0);
116 self
117 }
118
119 pub fn with_max_tokens(mut self, tokens: u32) -> Self {
121 self.max_tokens = tokens;
122 self
123 }
124}
125
126pub struct FastlyAgent {
128 config: FastlyAgentConfig,
129 backend_name: String,
130 messages: Vec<Message>,
131 provider: Provider,
132}
133
134impl FastlyAgent {
135 pub fn new(config: FastlyAgentConfig, backend_name: impl Into<String>) -> Result<Self> {
141 let provider = config
142 .provider
143 .parse::<Provider>()
144 .map_err(|_| FastlyAgentError::InvalidProvider(config.provider.clone()))?;
145
146 Ok(Self {
147 config,
148 backend_name: backend_name.into(),
149 messages: Vec::new(),
150 provider,
151 })
152 }
153
154 pub fn chat(&mut self, message: &str) -> Result<LlmResponse> {
156 self.messages.push(Message::user(message));
158
159 let mut builder = RequestBuilder::new(self.provider)
161 .model(&self.config.model)
162 .api_key(&self.config.api_key)
163 .temperature(self.config.temperature)
164 .max_tokens(self.config.max_tokens)
165 .stream(false);
166
167 if let Some(ref system) = self.config.system_prompt {
169 builder = builder.add_message(Message::system(system));
170 }
171
172 builder = builder.messages(&self.messages);
174
175 let http_request = builder.build()?;
176
177 let mut req = Request::new(Method::POST, &http_request.url);
179
180 for (key, value) in &http_request.headers {
181 req.set_header(key, value);
182 }
183
184 req.set_body(http_request.body);
185
186 let backend = Backend::from_name(&self.backend_name)
188 .map_err(|e| FastlyAgentError::Fastly(format!("Backend not found: {}", e)))?;
189
190 let resp = req.send(backend)?;
191
192 if resp.get_status() != StatusCode::OK {
194 let status = resp.get_status().as_u16();
195 let body = resp.into_body_str();
196 return Err(FastlyAgentError::Http {
197 status,
198 message: body,
199 });
200 }
201
202 let body = resp.into_body_str();
204 let response = ResponseParser::parse(self.provider, &body)?;
205
206 self.messages.push(Message::assistant(&response.content));
208
209 Ok(response)
210 }
211
212 pub fn chat_stream(&mut self, message: &str) -> Result<StreamingResponse> {
216 self.messages.push(Message::user(message));
218
219 let mut builder = RequestBuilder::new(self.provider)
221 .model(&self.config.model)
222 .api_key(&self.config.api_key)
223 .temperature(self.config.temperature)
224 .max_tokens(self.config.max_tokens)
225 .stream(true);
226
227 if let Some(ref system) = self.config.system_prompt {
229 builder = builder.add_message(Message::system(system));
230 }
231
232 builder = builder.messages(&self.messages);
234
235 let http_request = builder.build()?;
236
237 let mut req = Request::new(Method::POST, &http_request.url);
239
240 for (key, value) in &http_request.headers {
241 req.set_header(key, value);
242 }
243
244 req.set_body(http_request.body);
245
246 let backend = Backend::from_name(&self.backend_name)
248 .map_err(|e| FastlyAgentError::Fastly(format!("Backend not found: {}", e)))?;
249
250 let resp = req.send(backend)?;
251
252 if resp.get_status() != StatusCode::OK {
254 let status = resp.get_status().as_u16();
255 let body = resp.into_body_str();
256 return Err(FastlyAgentError::Http {
257 status,
258 message: body,
259 });
260 }
261
262 Ok(StreamingResponse {
263 body: resp.into_body(),
264 provider: self.provider,
265 buffer: String::new(),
266 accumulated_content: String::new(),
267 })
268 }
269
270 pub fn clear(&mut self) {
272 self.messages.clear();
273 }
274
275 pub fn history(&self) -> &[Message] {
277 &self.messages
278 }
279
280 pub fn add_assistant_message(&mut self, content: impl Into<String>) {
282 self.messages.push(Message::assistant(content));
283 }
284}
285
286pub struct StreamingResponse {
288 body: Body,
289 provider: Provider,
290 buffer: String,
291 accumulated_content: String,
292}
293
294impl StreamingResponse {
295 pub fn content(&self) -> &str {
297 &self.accumulated_content
298 }
299
300 pub fn next_chunk(&mut self) -> Result<Option<StreamChunk>> {
302 let mut chunk_buf = [0u8; 1024];
304 let bytes_read = self
305 .body
306 .read(&mut chunk_buf)
307 .map_err(|e| FastlyAgentError::Fastly(e.to_string()))?;
308
309 if bytes_read == 0 {
310 return Ok(None);
311 }
312
313 let chunk_str = String::from_utf8_lossy(&chunk_buf[..bytes_read]);
315 self.buffer.push_str(&chunk_str);
316
317 while let Some(line_end) = self.buffer.find('\n') {
319 let line = self.buffer[..line_end].to_string();
320 self.buffer = self.buffer[line_end + 1..].to_string();
321
322 if let Ok(Some(chunk)) = ResponseParser::parse_stream_line(self.provider, &line) {
323 if let Some(ref content) = chunk.content {
324 self.accumulated_content.push_str(content);
325 }
326 return Ok(Some(chunk));
327 }
328 }
329
330 self.next_chunk()
332 }
333}
334
335impl std::io::Read for StreamingResponse {
336 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
337 self.body.read(buf)
338 }
339}
340
341pub fn handle_chat_request(
345 incoming_req: Request,
346 config: FastlyAgentConfig,
347 backend_name: &str,
348) -> Result<Response> {
349 let body: serde_json::Value = serde_json::from_str(&incoming_req.into_body_str())?;
351
352 let message = body["message"]
353 .as_str()
354 .ok_or_else(|| FastlyAgentError::Fastly("Missing 'message' field".to_string()))?;
355
356 let mut agent = FastlyAgent::new(config, backend_name)?;
358 let response = agent.chat(message)?;
359
360 let response_body = serde_json::json!({
362 "content": response.content,
363 "usage": response.usage,
364 });
365
366 let mut resp = Response::from_body(response_body.to_string());
367 resp.set_header(header::CONTENT_TYPE, "application/json");
368
369 Ok(resp)
370}
371
372#[cfg(test)]
373mod tests {
374 use super::*;
375
376 #[test]
377 fn test_config_creation() {
378 let config = FastlyAgentConfig::new("openai", "sk-test", "gpt-4o-mini")
379 .with_system_prompt("You are helpful")
380 .with_temperature(0.5)
381 .with_max_tokens(2048);
382
383 assert_eq!(config.provider, "openai");
384 assert_eq!(config.model, "gpt-4o-mini");
385 assert_eq!(config.system_prompt, Some("You are helpful".to_string()));
386 assert_eq!(config.temperature, 0.5);
387 assert_eq!(config.max_tokens, 2048);
388 }
389
390 #[test]
391 fn test_provider_parsing() {
392 assert!("openai".parse::<Provider>().is_ok());
393 assert!("anthropic".parse::<Provider>().is_ok());
394 assert!("openrouter".parse::<Provider>().is_ok());
395 assert!("invalid".parse::<Provider>().is_err());
396 }
397}