1pub mod assistant;
79pub mod event;
81pub mod message;
83pub mod model;
85pub mod provider;
87pub mod reasoning;
89pub mod researcher;
91pub mod tool;
93
94use crate::llm::{model::Parameters, tool::Tools};
95use alloc::{
96 boxed::Box,
97 string::{String, ToString},
98 sync::Arc,
99 vec,
100 vec::Vec,
101};
102use core::{any::TypeId, future::Future};
103pub use event::{Event, ToolCall, Usage};
104use futures_core::Stream;
105use futures_lite::{StreamExt, pin};
106pub use message::{Attachment, Message, Role};
107pub use provider::LanguageModelProvider;
108pub use reasoning::ReasoningState;
109pub use researcher::{
110 ResearchCitation, ResearchEvent, ResearchFinding, ResearchOptions, ResearchReport,
111 ResearchRequest, ResearchSource, ResearchStage, Researcher, ResearcherProfile,
112};
113use schemars::{JsonSchema, schema_for};
114use serde::de::DeserializeOwned;
115pub use tool::{
116 CommandOutcome, CommandResult, CommandStream, IntoToolResult, StoredFile, Tool, ToolContext,
117 ToolResult,
118};
119
120use crate::llm::model::Profile;
121
122#[derive(Debug)]
131pub enum GenerateError<E> {
132 Provider(E),
134
135 Parse {
139 source: serde_json::Error,
141 response: String,
143 },
144}
145
146impl<E: core::fmt::Display> core::fmt::Display for GenerateError<E> {
147 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
148 match self {
149 Self::Provider(err) => write!(f, "language model request failed: {err}"),
150 Self::Parse { source, response } => {
151 write!(
152 f,
153 "structured output did not match the requested schema: {source}; response: {response}"
154 )
155 }
156 }
157 }
158}
159
160impl<E: core::error::Error + 'static> core::error::Error for GenerateError<E> {
161 fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
162 match self {
163 Self::Provider(err) => Some(err),
164 Self::Parse { source, .. } => Some(source),
165 }
166 }
167}
168
169#[derive(Debug, Clone)]
174pub struct LLMRequest {
175 messages: Vec<Message>,
176 parameters: Parameters,
177 tool_definitions: Vec<tool::ToolDefinition>,
178}
179
180impl LLMRequest {
181 pub fn new(messages: impl Into<Vec<Message>>) -> Self {
183 Self {
184 messages: messages.into(),
185 parameters: Parameters::default(),
186 tool_definitions: Vec::new(),
187 }
188 }
189
190 #[must_use]
195 pub fn with_tool_definitions(mut self, definitions: Vec<tool::ToolDefinition>) -> Self {
196 self.tool_definitions = definitions;
197 self
198 }
199
200 #[must_use]
202 pub fn with_tool<T: Tool>(mut self, tool: &T) -> Self {
203 self.tool_definitions.push(tool::ToolDefinition::new(tool));
204 self
205 }
206
207 #[must_use]
209 pub fn with_parameters(mut self, parameters: Parameters) -> Self {
210 self.parameters = parameters;
211 self
212 }
213
214 #[must_use]
216 pub fn messages(&self) -> &[Message] {
217 &self.messages
218 }
219
220 pub const fn messages_mut(&mut self) -> &mut Vec<Message> {
222 &mut self.messages
223 }
224
225 #[must_use]
227 pub const fn parameters(&self) -> &Parameters {
228 &self.parameters
229 }
230
231 #[must_use]
233 pub fn tool_definitions(&self) -> &[tool::ToolDefinition] {
234 &self.tool_definitions
235 }
236
237 #[must_use]
239 pub fn into_parts(self) -> (Vec<Message>, Parameters, Vec<tool::ToolDefinition>) {
240 (self.messages, self.parameters, self.tool_definitions)
241 }
242}
243
244#[derive(Debug)]
249pub struct LLMRequestWithTools<'tools> {
250 inner: LLMRequest,
251 tools: &'tools mut Tools,
252}
253
254impl LLMRequest {
255 pub fn with_tools(self, tools: &mut Tools) -> LLMRequestWithTools<'_> {
260 let definitions = tools.definitions();
261 LLMRequestWithTools {
262 inner: self.with_tool_definitions(definitions),
263 tools,
264 }
265 }
266}
267
268impl<'tools> LLMRequestWithTools<'tools> {
269 #[must_use]
271 pub const fn request(&self) -> &LLMRequest {
272 &self.inner
273 }
274
275 #[must_use]
277 pub const fn tools(&mut self) -> &mut Tools {
278 self.tools
279 }
280
281 #[must_use]
283 pub fn into_parts(self) -> (LLMRequest, &'tools mut Tools) {
284 (self.inner, self.tools)
285 }
286
287 pub async fn call_tool(
293 &mut self,
294 name: &str,
295 args_json: &str,
296 cx: ToolContext,
297 ) -> crate::Result<ToolResult> {
298 self.tools.call(name, args_json, cx).await
299 }
300}
301
302pub trait LanguageModel: Sized + Send + Sync {
310 type Error: core::error::Error + Send + Sync + 'static;
312
313 fn respond(&self, request: LLMRequest)
321 -> impl Stream<Item = Result<Event, Self::Error>> + Send;
322
323 fn respond_with_tools(
328 &self,
329 request: LLMRequestWithTools<'_>,
330 ) -> impl Stream<Item = Result<Event, Self::Error>> + Send {
331 let (inner, _tools) = request.into_parts();
332 self.respond(inner)
333 }
334
335 fn generate<T: JsonSchema + DeserializeOwned + 'static>(
342 &self,
343 request: LLMRequest,
344 ) -> impl Future<Output = Result<T, GenerateError<Self::Error>>> + Send {
345 async { structured_generate(self, request).await }
346 }
347
348 fn complete(&self, prefix: &str) -> impl Stream<Item = Result<Event, Self::Error>> + Send {
350 self.respond(oneshot("Please complete the following text:", prefix))
351 }
352
353 fn summarize(&self, text: &str) -> impl Stream<Item = Result<Event, Self::Error>> + Send {
359 summarize(self, text)
360 }
361
362 fn categorize<T: JsonSchema + DeserializeOwned + 'static>(
368 &self,
369 text: &str,
370 ) -> impl Future<Output = Result<T, GenerateError<Self::Error>>> + Send {
371 async { categorize_text(self, text).await }
372 }
373
374 fn profile(&self) -> impl Future<Output = Result<Profile, Self::Error>> + Send;
384}
385
386macro_rules! impl_language_model {
387 ($($name:ident),*) => {
388 $(
389 impl<T: LanguageModel> LanguageModel for $name<T> {
390 type Error = T::Error;
391
392 fn respond(
393 &self,
394 request: LLMRequest,
395 ) -> impl Stream<Item = Result<Event, Self::Error>> + Send {
396 T::respond(self, request)
397 }
398
399 fn respond_with_tools(
400 &self,
401 request: LLMRequestWithTools<'_>,
402 ) -> impl Stream<Item = Result<Event, Self::Error>> + Send {
403 T::respond_with_tools(self, request)
404 }
405
406 fn generate<U: JsonSchema + DeserializeOwned + 'static>(
407 &self,
408 request: LLMRequest,
409 ) -> impl Future<Output = Result<U, GenerateError<Self::Error>>> + Send {
410 T::generate(self, request)
411 }
412
413 fn complete(
414 &self,
415 prefix: &str,
416 ) -> impl Stream<Item = Result<Event, Self::Error>> + Send {
417 T::complete(self, prefix)
418 }
419
420 fn summarize(
421 &self,
422 text: &str,
423 ) -> impl Stream<Item = Result<Event, Self::Error>> + Send {
424 T::summarize(self, text)
425 }
426
427 fn categorize<U: JsonSchema + DeserializeOwned + 'static>(
428 &self,
429 text: &str,
430 ) -> impl Future<Output = Result<U, GenerateError<Self::Error>>> + Send {
431 T::categorize(self, text)
432 }
433
434 fn profile(&self) -> impl Future<Output = Result<Profile, Self::Error>> + Send {
435 T::profile(self)
436 }
437 }
438 )*
439 };
440}
441
442impl<T: LanguageModel> LanguageModel for &T {
443 type Error = T::Error;
444
445 fn respond(
446 &self,
447 request: LLMRequest,
448 ) -> impl Stream<Item = Result<Event, Self::Error>> + Send {
449 T::respond(self, request)
450 }
451
452 fn respond_with_tools(
453 &self,
454 request: LLMRequestWithTools<'_>,
455 ) -> impl Stream<Item = Result<Event, Self::Error>> + Send {
456 T::respond_with_tools(self, request)
457 }
458
459 fn generate<U: JsonSchema + DeserializeOwned + 'static>(
460 &self,
461 request: LLMRequest,
462 ) -> impl Future<Output = Result<U, GenerateError<Self::Error>>> + Send {
463 T::generate(self, request)
464 }
465
466 fn complete(&self, prefix: &str) -> impl Stream<Item = Result<Event, Self::Error>> + Send {
467 T::complete(self, prefix)
468 }
469
470 fn summarize(&self, text: &str) -> impl Stream<Item = Result<Event, Self::Error>> + Send {
471 T::summarize(self, text)
472 }
473
474 fn categorize<U: JsonSchema + DeserializeOwned + 'static>(
475 &self,
476 text: &str,
477 ) -> impl Future<Output = Result<U, GenerateError<Self::Error>>> + Send {
478 T::categorize(self, text)
479 }
480
481 fn profile(&self) -> impl Future<Output = Result<Profile, Self::Error>> + Send {
482 T::profile(self)
483 }
484}
485
486mod prompts;
487
488impl_language_model!(Arc, Box);
489
490pub async fn collect_text<S, E>(stream: S) -> Result<String, E>
496where
497 S: Stream<Item = Result<Event, E>>,
498{
499 pin!(stream);
500 let mut result = String::new();
501 while let Some(event) = stream.next().await {
502 if let Event::Text(text) = event? {
503 result.push_str(&text);
504 }
505 }
506 Ok(result)
507}
508
509async fn structured_generate<T: JsonSchema + DeserializeOwned + 'static, M: LanguageModel>(
510 model: &M,
511 mut request: LLMRequest,
512) -> Result<T, GenerateError<M::Error>> {
513 let schema = schema_for!(T);
514
515 let json = if schema.as_value().is_string() {
517 let stream = model.respond(request);
518 let response = collect_text(stream)
519 .await
520 .map_err(GenerateError::Provider)?;
521 serde_json::to_string(&response).map_err(|source| GenerateError::Parse {
523 source,
524 response: response.clone(),
525 })?
526 } else {
527 let schema =
530 serde_json::to_string_pretty(&schema).map_err(|source| GenerateError::Parse {
531 source,
532 response: String::new(),
533 })?;
534 let prompt = prompts::generate(&schema);
535 request.messages.push(Message::system(prompt));
536 request.parameters.structured_outputs = true;
537
538 let stream = model.respond(request);
539 collect_text(stream)
540 .await
541 .map_err(GenerateError::Provider)?
542 };
543
544 parse_json_with_recovery(&json).map_err(|source| GenerateError::Parse {
545 source,
546 response: truncate_for_error(&json),
547 })
548}
549
550fn truncate_for_error(response: &str) -> String {
552 const LIMIT: usize = 500;
553 response.chars().take(LIMIT).collect()
554}
555
556pub fn oneshot(system: impl Into<String>, user: impl Into<String>) -> LLMRequest {
558 let messages = vec![Message::system(system.into()), Message::user(user.into())];
559 LLMRequest::new(messages)
560}
561
562fn summarize<M: LanguageModel>(
563 model: &M,
564 text: &str,
565) -> impl Stream<Item = Result<Event, M::Error>> + Send {
566 let messages = oneshot("Summarize text:", text);
567 model.respond(messages)
568}
569
570async fn categorize_text<T: JsonSchema + DeserializeOwned + 'static, M: LanguageModel>(
571 model: &M,
572 text: &str,
573) -> Result<T, GenerateError<M::Error>> {
574 let request = oneshot("Categorize text by provided schema", text);
575 model.generate(request).await
576}
577
578fn parse_json_with_recovery<T: DeserializeOwned + 'static>(
579 json: &str,
580) -> Result<T, serde_json::Error> {
581 use serde::de::Error as _;
582
583 let trimmed = json.trim();
584 let mut last_error: Option<serde_json::Error> = None;
585 let mut last_candidate: Option<String> = None;
586
587 for candidate in build_json_candidates(trimmed) {
588 match serde_json::from_str::<T>(&candidate) {
589 Ok(value) => return Ok(value),
590 Err(err) => {
591 last_error = Some(err);
592 last_candidate = Some(candidate);
593 }
594 }
595 }
596
597 if is_string_type::<T>()
600 && let Some(candidate) = last_candidate
601 && let Ok(value) = serde_json::from_str::<serde_json::Value>(&candidate)
602 {
603 let text = match value {
604 serde_json::Value::String(s) => s,
605 other => other.to_string(),
606 };
607 let encoded = serde_json::to_string(&text)?;
608 if let Ok(value) = serde_json::from_str::<T>(&encoded) {
609 return Ok(value);
610 }
611 }
612
613 Err(last_error.unwrap_or_else(|| {
614 serde_json::Error::custom("structured output was empty or missing a JSON block")
615 }))
616}
617
618fn strip_code_fences(raw: &str) -> Option<String> {
619 let trimmed = raw.trim();
620 let fence_start = trimmed.find("```")?;
621 let after_fence = &trimmed[fence_start + 3..];
622 let mut lines = after_fence.lines();
623 let _maybe_lang = lines.next();
624 let body = lines.collect::<Vec<_>>().join("\n");
625 let content = body.rfind("```").map_or(body.as_str(), |end| &body[..end]);
626
627 let cleaned = content.trim();
628 if cleaned.is_empty() {
629 None
630 } else {
631 Some(cleaned.to_string())
632 }
633}
634
635fn extract_json_block(raw: &str) -> Option<String> {
636 if let (Some(start), Some(end)) = (raw.find('{'), raw.rfind('}'))
637 && end >= start
638 {
639 let candidate = &raw[start..=end];
640 if !candidate.trim().is_empty() {
641 return Some(candidate.trim().to_string());
642 }
643 }
644 if let (Some(start), Some(end)) = (raw.find('['), raw.rfind(']'))
645 && end >= start
646 {
647 let candidate = &raw[start..=end];
648 if !candidate.trim().is_empty() {
649 return Some(candidate.trim().to_string());
650 }
651 }
652 None
653}
654
655fn build_json_candidates(raw: &str) -> Vec<String> {
656 let mut candidates = Vec::new();
657
658 if !raw.is_empty() {
659 candidates.push(raw.to_string());
660 }
661
662 if let Some(fenced) = strip_code_fences(raw) {
663 candidates.push(fenced);
664 }
665
666 if let Some(block) = extract_json_block(raw) {
667 candidates.push(block);
668 }
669
670 if let Some(dequoted) = dequote_json_string(raw) {
671 candidates.push(dequoted);
672 }
673
674 if let Some(stripped) = strip_leading_label(raw, "json") {
675 candidates.push(stripped);
676 }
677
678 let mut deduped = Vec::new();
679 for candidate in candidates {
680 if deduped.iter().all(|seen| seen != &candidate) {
681 deduped.push(candidate);
682 }
683 }
684 deduped
685}
686
687fn dequote_json_string(raw: &str) -> Option<String> {
688 let trimmed = raw.trim();
689 if !(trimmed.starts_with('"') && trimmed.ends_with('"')) {
690 return None;
691 }
692 let inner: String = serde_json::from_str(trimmed).ok()?;
693 if inner.trim().is_empty() {
694 None
695 } else {
696 Some(inner)
697 }
698}
699
700fn strip_leading_label(raw: &str, label: &str) -> Option<String> {
701 let trimmed = raw.trim_start();
702 if !trimmed.to_ascii_lowercase().starts_with(label) {
703 return None;
704 }
705 let stripped = trimmed[label.len()..]
706 .trim_start_matches(|c: char| c.is_whitespace() || c == ':' || c == '-')
707 .trim();
708 if stripped.is_empty() {
709 None
710 } else {
711 Some(stripped.to_string())
712 }
713}
714
715fn is_string_type<T: 'static>() -> bool {
716 TypeId::of::<T>() == TypeId::of::<String>()
717}
718
719#[cfg(test)]
720mod tests {
721 use super::parse_json_with_recovery;
722 use alloc::string::String;
723 use serde::Deserialize;
724
725 #[derive(Debug, Deserialize, PartialEq, Eq)]
726 struct Foo {
727 a: u8,
728 }
729
730 #[test]
731 fn parses_plain_json() {
732 let foo: Foo = parse_json_with_recovery(r#"{"a":1}"#).unwrap();
733 assert_eq!(foo, Foo { a: 1 });
734 }
735
736 #[test]
737 fn parses_code_fence_json() {
738 let foo: Foo = parse_json_with_recovery("```json\n{\"a\":2}\n```").unwrap();
739 assert_eq!(foo, Foo { a: 2 });
740 }
741
742 #[test]
743 fn parses_embedded_block() {
744 let foo: Foo = parse_json_with_recovery("noise {\"a\":3} trailing").unwrap();
745 assert_eq!(foo, Foo { a: 3 });
746 }
747
748 #[test]
749 fn parses_quoted_json_string() {
750 let foo: Foo = parse_json_with_recovery(r#""{\"a\":4}""#).unwrap();
751 assert_eq!(foo, Foo { a: 4 });
752 }
753
754 #[test]
755 fn parses_labeled_json() {
756 let foo: Foo = parse_json_with_recovery("json {\"a\":5}").unwrap();
757 assert_eq!(foo, Foo { a: 5 });
758 }
759
760 #[test]
761 fn coerces_object_to_string() {
762 let value: String =
763 parse_json_with_recovery(r#"{"title":"summary","type":"content"}"#).unwrap();
764 assert!(
765 value.contains("\"title\":\"summary\"") && value.contains("\"type\":\"content\""),
766 "unexpected value: {value}"
767 );
768 }
769}