2024-09-12 13:44:06 +03:00
use std ::fmt ::Debug ;
use std ::sync ::Arc ;
2025-02-27 09:31:53 +02:00
use anthropic ::client ::{ Client , ClientBuilder } ;
use anthropic ::types ::ContentBlock ;
2024-09-12 13:44:06 +03:00
use super ::super ::ControllerTrait ;
2025-02-27 07:46:16 +02:00
use crate ::agent ::AgentPurpose ;
2024-09-12 13:44:06 +03:00
use crate ::agent ::provider ::entity ::{
ImageGenerationResult , PingResult , TextGenerationParams , TextGenerationResult ,
TextToSpeechParams , TextToSpeechResult ,
} ;
use crate ::agent ::provider ::{ ImageGenerationParams , SpeechToTextParams , SpeechToTextResult } ;
use crate ::conversation ::llm ::{
2025-02-27 07:46:16 +02:00
Author as LLMAuthor , Conversation as LLMConversation , Message as LLMMessage ,
shorten_messages_list_to_context_size ,
2024-09-12 13:44:06 +03:00
} ;
use crate ::strings ;
use super ::config ::Config ;
struct ControllerInner {
2025-02-27 09:31:53 +02:00
client : Client ,
2024-09-12 13:44:06 +03:00
}
#[ derive(Clone) ]
pub struct Controller {
config : Config ,
inner : Arc < ControllerInner > ,
}
impl Debug for Controller {
fn fmt ( & self , f : & mut std ::fmt ::Formatter < '_ > ) -> std ::fmt ::Result {
f . debug_struct ( " Controller " )
. field ( " config " , & self . config )
. finish ( )
}
}
impl Controller {
pub fn new ( config : Config ) -> anyhow ::Result < Self > {
2025-02-27 09:31:53 +02:00
// The previous library that we used expected a base URL that ends with "/v1"
// (e.g. "https://api.anthropic.com/v1"), while the new one doesn't.
//
// To keep backward compatibility, we don't ask people to change their configuration
// and rather adapt by removing the "/v1" from the base URL.
if ! config . base_url . ends_with ( " /v1 " ) {
return Err ( anyhow ::anyhow! ( " base_url must end with '/v1' " ) ) ;
}
let base_url = & config . base_url [ .. config . base_url . len ( ) - 3 ] ;
let client = ClientBuilder ::default ( )
. api_base ( base_url . to_string ( ) )
. api_key ( config . api_key . clone ( ) )
. build ( ) ? ;
2024-09-12 13:44:06 +03:00
Ok ( Self {
config ,
inner : Arc ::new ( ControllerInner { client } ) ,
} )
}
}
impl ControllerTrait for Controller {
async fn ping ( & self ) -> anyhow ::Result < PingResult > {
if ! self . supports_purpose ( AgentPurpose ::TextGeneration ) {
return Ok ( PingResult ::Inconclusive ) ;
}
let messages = vec! [ LLMMessage {
author : LLMAuthor ::User ,
message_text : " Hello! " . to_string ( ) ,
2024-10-03 11:48:14 +03:00
timestamp : chrono ::Utc ::now ( ) ,
2024-09-12 13:44:06 +03:00
} ] ;
let conversation = LLMConversation { messages } ;
self . generate_text ( conversation , TextGenerationParams ::default ( ) )
. await ? ;
Ok ( PingResult ::Successful )
}
async fn generate_text (
& self ,
conversation : LLMConversation ,
params : TextGenerationParams ,
) -> anyhow ::Result < TextGenerationResult > {
let Some ( text_generation_config ) = & self . config . text_generation else {
return Err ( anyhow ::anyhow! (
strings ::agent ::no_configuration_for_purpose_so_cannot_be_used (
& AgentPurpose ::TextGeneration
) ,
) ) ;
} ;
2024-09-21 14:05:19 +00:00
let prompt_text = params . prompt_variables . format (
params
. prompt_override
. unwrap_or ( self . text_generation_prompt ( ) . unwrap_or ( " " . to_owned ( ) ) )
. trim ( ) ,
) ;
2024-09-12 13:44:06 +03:00
let prompt_message = if prompt_text . is_empty ( ) {
None
} else {
Some ( LLMMessage {
author : LLMAuthor ::Prompt ,
message_text : prompt_text ,
2024-10-03 11:48:14 +03:00
timestamp : chrono ::Utc ::now ( ) ,
2024-09-12 13:44:06 +03:00
} )
} ;
2024-09-22 06:39:00 +00:00
// Avoid the situation where multiple user or assistant messages are sent consecutively,
// to avoid errors like:
// > API error: Error response: error Api error: invalid_request_error messages: roles must alternate between "user" and "assistant", but found multiple "user" roles in a row
// as reported here: https://github.com/etkecc/baibot/issues/13
//
// As https://docs.anthropic.com/en/api/messages says:
// > Our models are trained to operate on alternating user and assistant conversational turns.
let conversation = conversation . combine_consecutive_messages ( ) ;
2024-09-12 13:44:06 +03:00
let mut conversation_messages = conversation . messages ;
if params . context_management_enabled {
tracing ::trace! ( " Shortening messages list to context size " ) ;
conversation_messages = shorten_messages_list_to_context_size (
& text_generation_config . model_id ,
& prompt_message ,
conversation_messages ,
2024-10-03 10:36:01 +03:00
Some ( text_generation_config . max_response_tokens ) ,
2024-09-12 13:44:06 +03:00
text_generation_config . max_context_tokens ,
) ;
tracing ::trace! ( " Finished shortening messages list to context size " ) ;
} ;
let messages_count = conversation_messages . len ( ) ;
let mut request = super ::utils ::create_anthropic_message_request ( conversation_messages ) ;
let temperature = params
. temperature_override
. unwrap_or ( text_generation_config . temperature ) ;
if let Some ( prompt_message ) = prompt_message {
2025-02-27 09:31:53 +02:00
request . system = prompt_message . message_text ;
2024-09-12 13:44:06 +03:00
}
2025-02-27 09:31:53 +02:00
request . model = text_generation_config . model_id . clone ( ) ;
request . temperature = Some ( temperature as f64 ) ;
request . max_tokens = text_generation_config . max_response_tokens as usize ;
2024-09-12 13:44:06 +03:00
if let Ok ( request_as_json ) = serde_json ::to_string ( & request ) {
tracing ::trace! (
model = format! ( " {:?} " , request . model ) ,
? messages_count ,
request = request_as_json ,
" Sending Anthropic create message API request "
) ;
}
2025-02-27 09:31:53 +02:00
let response = self . inner . client . messages ( request ) . await ? ;
2024-09-12 13:44:06 +03:00
tracing ::trace! ( ? response , " Got response from Anthropic create message API " ) ;
// response.content usually contains a single element, but we support handling multiple to account for all possibilities
let mut text_parts = vec! [ ] ;
for content in response . content {
2025-02-27 09:31:53 +02:00
match content {
ContentBlock ::Text { text } = > {
text_parts . push ( text ) ;
}
ContentBlock ::Image {
source ,
media_type ,
data : _ ,
} = > {
text_parts . push ( format! (
" The model responded with an image of type {}: {} " ,
media_type , source
) ) ;
}
2024-09-12 13:44:06 +03:00
}
}
if text_parts . is_empty ( ) {
return Err ( anyhow ::anyhow! (
" No text content in response from the Anthropic create message API "
) ) ;
}
Ok ( TextGenerationResult {
text : text_parts . join ( " \n \n " ) ,
} )
}
async fn speech_to_text (
& self ,
_mime_type : & mxlink ::mime ::Mime ,
_media : Vec < u8 > ,
_params : SpeechToTextParams ,
) -> anyhow ::Result < SpeechToTextResult > {
Err ( anyhow ::anyhow! ( " Speech-to-Text not supported " ) )
}
async fn generate_image (
& self ,
_prompt : & str ,
_params : ImageGenerationParams ,
) -> anyhow ::Result < ImageGenerationResult > {
Err ( anyhow ::anyhow! ( " Image generation not supported " ) )
}
async fn text_to_speech (
& self ,
_input : & str ,
_params : TextToSpeechParams ,
) -> anyhow ::Result < TextToSpeechResult > {
Err ( anyhow ::anyhow! ( " Speech generation not supported " ) )
}
fn supports_purpose ( & self , purpose : AgentPurpose ) -> bool {
match purpose {
AgentPurpose ::TextGeneration = > self . config . text_generation . is_some ( ) ,
AgentPurpose ::SpeechToText = > false ,
AgentPurpose ::TextToSpeech = > false ,
AgentPurpose ::ImageGeneration = > false ,
AgentPurpose ::CatchAll = > true ,
}
}
2024-09-21 14:05:19 +00:00
fn text_generation_model_id ( & self ) -> Option < String > {
self . config
. text_generation
. as_ref ( )
. map ( | config | config . model_id . to_owned ( ) )
}
2024-09-12 13:44:06 +03:00
2024-09-21 14:05:19 +00:00
fn text_generation_prompt ( & self ) -> Option < String > {
self . config
. text_generation
. as_ref ( )
. and_then ( | config | config . prompt . clone ( ) )
2024-09-12 13:44:06 +03:00
}
fn text_generation_temperature ( & self ) -> Option < f32 > {
2024-09-21 14:05:19 +00:00
self . config
. text_generation
. as_ref ( )
. map ( | config | config . temperature )
2024-09-12 13:44:06 +03:00
}
fn text_to_speech_voice ( & self ) -> Option < String > {
None
}
fn text_to_speech_speed ( & self ) -> Option < f32 > {
None
}
}