mod tools;
use std::{env, pin::Pin};
use anyhow::{Error, Result};
use tokio::sync::mpsc::{Receiver, Sender};
use crate::{
config::{Config, Endpoint, EndpointSecrets},
core::conversation::ConversationItemKind,
session::SessionMessage,
};
use futures::{Stream, StreamExt};
use rig::{
Agent,
agent::{MultiTurnStreamItem, StreamingError},
client::{AgentClientExt, Client},
providers::openai::{
self, OpenAIResponsesExt,
responses_api::{GenericResponsesCompletionModel, streaming::StreamingCompletionResponse},
},
streaming::{StreamedAssistantContent, StreamingChat},
};
#[derive(Clone, Debug, PartialEq)]
pub enum LlmMessage {
SubmitPrompt {
prompt: String,
history: Vec<ConversationItemKind>,
},
Quit,
}
type ResponseStream = Pin<
Box<
dyn Stream<Item = Result<MultiTurnStreamItem<StreamingCompletionResponse>, StreamingError>>
+ Send
+ 'static,
>,
>;
pub enum LlmState {
Idle,
Requesting {
user_prompt: String,
history: Vec<rig::completion::Message>,
},
Streaming {
stream: ResponseStream,
},
Quitting,
}
#[derive(Debug)]
pub struct Llm {
session_tx: Sender<SessionMessage>,
rx: Receiver<LlmMessage>,
config: Config,
}
impl Llm {
pub fn new(
session_tx: Sender<SessionMessage>,
rx: Receiver<LlmMessage>,
config: Config,
) -> Self {
Self {
session_tx,
rx,
config,
}
}
pub async fn run(&mut self) {
let endpoint = self.get_active_endpoint().expect("Endpoint missing");
let client = self.connect(&endpoint.base_url, endpoint.secrets).unwrap();
let base_dir = env::current_dir().expect("Cannot get current directory");
let agent = client
.agent(endpoint.default_model)
.preamble("You are a experienced software developer advising a colleague.")
.tool(tools::ListDir::new(
&base_dir,
self.config.general.max_list_dir_results,
))
.tool(tools::ReadFile::new(&base_dir))
.tool(tools::SearchText::new(
&base_dir,
self.config.general.max_search_text_results,
))
.default_max_turns(self.config.general.max_iterations_per_request)
.max_tokens(self.config.general.max_tokens)
.build();
let mut state = LlmState::Idle;
loop {
state = match state {
LlmState::Idle => self.handle_idle().await,
LlmState::Requesting {
user_prompt,
history,
} => self.handle_requesting(&agent, user_prompt, history).await,
LlmState::Streaming { stream } => self.handle_streaming(stream).await,
LlmState::Quitting => break,
};
}
}
fn connect(
&self,
base_url: &str,
secrets: Option<EndpointSecrets>,
) -> Result<Client<OpenAIResponsesExt>> {
let api_token = secrets
.unwrap_or(EndpointSecrets {
api_token: Some("".to_string()),
})
.api_token
.unwrap_or("".to_string());
let client = openai::Client::builder()
.base_url(base_url)
.api_key(api_token)
.build()?;
Ok(client)
}
fn get_active_endpoint(&self) -> Result<Endpoint> {
self.config
.endpoint
.get(&self.config.general.primary_endpoint)
.map(|val| val.to_owned())
.ok_or(Error::msg(format!(
"Endpoint {} not found in endpoints.",
self.config.general.primary_endpoint
)))
}
async fn handle_idle(&mut self) -> LlmState {
match self.rx.recv().await {
Some(LlmMessage::SubmitPrompt { prompt, history }) => {
let history: Vec<rig::completion::Message> = Vec::new();
LlmState::Requesting {
user_prompt: prompt,
history,
}
}
Some(LlmMessage::Quit) => LlmState::Quitting,
None => LlmState::Quitting,
}
}
async fn handle_requesting(
&mut self,
agent: &Agent<GenericResponsesCompletionModel>,
user_prompt: String,
history: Vec<rig::completion::Message>,
) -> LlmState {
let request = agent.stream_chat(user_prompt, history);
tokio::select! {
stream = request => { LlmState::Streaming { stream } }
Some(LlmMessage::Quit) = self.rx.recv() => { LlmState::Idle }
}
}
async fn handle_streaming(&mut self, mut stream: ResponseStream) -> LlmState {
loop {
tokio::select! {
maybe_chunk = stream.next() => {
if let Some(result) = maybe_chunk{
if let Ok(chunk) = result {
self.handle_llm_item(chunk).await;
} else {
self.session_tx.send(SessionMessage::Error(format!("{:?}", result))).await.unwrap();
}
} else {
return LlmState::Idle;
}
}
Some(LlmMessage::Quit) = self.rx.recv() => { return LlmState::Idle; }
}
}
}
async fn handle_llm_item(&mut self, chunk: MultiTurnStreamItem<StreamingCompletionResponse>) {
match chunk {
MultiTurnStreamItem::StreamAssistantItem(streamed_assistant_content) => {
match streamed_assistant_content {
StreamedAssistantContent::Text(text) => {
self.session_tx
.send(SessionMessage::AgentResponse(text.to_string()))
.await
.unwrap();
}
StreamedAssistantContent::ToolCall {
tool_call,
internal_call_id: _,
} => {
self.session_tx
.send(SessionMessage::ToolCall {
name: tool_call.function.name,
args: tool_call.function.arguments,
})
.await
.unwrap();
}
StreamedAssistantContent::ToolCallDelta {
id: _,
internal_call_id: _,
content: _,
} => {
}
StreamedAssistantContent::Reasoning(reasoning) => {
}
StreamedAssistantContent::ReasoningDelta { id: _, reasoning } => {
self.session_tx
.send(SessionMessage::AgentReasoning(reasoning))
.await
.unwrap();
}
StreamedAssistantContent::Final(message) => {
}
StreamedAssistantContent::Unknown(value) => {
}
}
}
MultiTurnStreamItem::ToolExecutionCommitted {
tool_call: _,
internal_call_id: _,
} => {
}
MultiTurnStreamItem::StreamUserItem(
rig::streaming::StreamedUserContent::ToolResult {
tool_result,
internal_call_id: _,
},
) => {
self.session_tx
.send(SessionMessage::ToolResult {
output: format!("{:?}", tool_result.content),
success: true, })
.await
.unwrap();
}
MultiTurnStreamItem::CompletionCall(completion_call)
if completion_call.usage.has_values() =>
{
self.session_tx
.send(SessionMessage::AgentCompletion(completion_call.usage))
.await
.unwrap();
}
MultiTurnStreamItem::ModelTurnRetried { turn } => {
}
MultiTurnStreamItem::FinalResponse(prompt_response) => {
}
_ => {
}
}
}
}
async fn next_if_some<S>(s: &mut Option<S>) -> Option<S::Item>
where
S: Stream + Unpin,
{
match s.as_mut() {
Some(stream) => stream.next().await,
None => std::future::pending().await,
}
}