use crate::domain::{Agent, Message, MessageTarget};
use crate::infrastructure::llm::{OpenAIClient, Tool, ToolCall, ToolResponse};
use anyhow::Result;
pub struct AgentRuntime {
agent: Agent,
llm: OpenAIClient,
}
impl AgentRuntime {
pub async fn new(agent: Agent) -> Result<Self> {
let llm = OpenAIClient::new_with_base_url(
agent.llm_config.api_key.clone(),
agent.llm_config.model.clone(),
agent.llm_config.base_url.clone(),
);
Ok(Self { agent, llm })
}
pub fn id(&self) -> &str {
&self.agent.id
}
pub fn name(&self) -> &str {
&self.agent.name
}
pub fn agent(&self) -> &Agent {
&self.agent
}
pub async fn react_think(&self, context: Context, tools: Vec<Tool>) -> Result<Decision> {
let mut current_context = context;
let max_iterations = 5; for _iteration in 0..max_iterations {
let reasoning_result = self.reason_step(¤t_context, &tools).await?;
match reasoning_result {
ReActStepResult::Action {
tool_calls,
observation,
} => {
let observations = self.execute_actions(tool_calls, ¤t_context).await?;
let obs_text = format!("{}\n{}", observation, observations.join("\n"));
current_context = current_context.with_observation(obs_text);
}
ReActStepResult::FinalDecision(decision) => {
return Ok(decision);
}
}
}
Ok(Decision::ExecuteTask {
task: "Final response after multiple reasoning steps".to_string(),
})
}
async fn reason_step(&self, context: &Context, tools: &[Tool]) -> Result<ReActStepResult> {
let prompt = self.build_react_prompt(context, tools);
let response = self.llm.chat_with_tools(
vec![
crate::infrastructure::llm::Message::system(prompt),
crate::infrastructure::llm::Message::user("Think step by step and decide what actions to take. If you need to use tools, specify them. Otherwise, provide your final response.")
],
tools.to_vec()
).await?;
match response {
ToolResponse::Message(content) => {
Ok(ReActStepResult::FinalDecision(Decision::ExecuteTask {
task: content,
}))
}
ToolResponse::ToolCalls {
content,
tool_calls,
} => {
Ok(ReActStepResult::Action {
tool_calls,
observation: content,
})
}
}
}
async fn execute_actions(
&self,
tool_calls: Vec<ToolCall>,
_context: &Context,
) -> Result<Vec<String>> {
let mut observations = Vec::new();
for tool_call in tool_calls {
let observation = format!(
"Executed tool '{}' with arguments: {}. Result: Tool executed successfully.",
tool_call.name, tool_call.arguments
);
observations.push(observation);
}
Ok(observations)
}
fn build_react_prompt(&self, context: &Context, tools: &[Tool]) -> String {
let mut prompt = format!(
"You are {}, {}. ",
self.agent.name,
self.agent.system_prompt()
);
if let Some(org_info) = &context.organization_info {
prompt.push_str(&format!("Organization info: {}\n", org_info));
}
if let Some(task) = &context.current_task {
prompt.push_str(&format!("Your current task is: {}\n", task));
}
if !context.unread_messages.is_empty() {
prompt.push_str("\nRecent messages:\n");
for msg in &context.unread_messages {
let target_desc = match &msg.to {
MessageTarget::Direct(id) => format!("Direct to {}", id),
MessageTarget::Group(id) => format!("Group {}", id),
};
prompt.push_str(&format!(
"- From {}: {} (to: {})\n",
msg.from, msg.content, target_desc
));
}
}
if !context.observations.is_empty() {
prompt.push_str("\nPrevious observations:\n");
for obs in &context.observations {
prompt.push_str(&format!("- {}\n", obs));
}
}
if !tools.is_empty() {
prompt.push_str("\nYou have access to these tools:\n");
for tool in tools {
prompt.push_str(&format!("- {}: {}\n", tool.name, tool.description));
}
}
prompt.push_str("\nFollow the ReAct (Reasoning and Acting) framework:\n");
prompt.push_str("1. Think: Analyze the situation and plan your approach\n");
prompt.push_str("2. Act: Use tools or take actions as needed\n");
prompt.push_str("3. Observe: See the results of your actions\n");
prompt.push_str("4. Repeat: Continue until you achieve your goal\n");
prompt.push_str("\nProvide your response in JSON format with fields: 'action' (one of 'send_message', 'create_group', 'execute_task', 'wait', or use available tools), 'target' (for messages), 'content' (for messages), 'group_name' (for groups), 'members' (for groups), 'task' (for execute_task).");
prompt
}
pub async fn think(&self, context: Context) -> Result<Decision> {
let prompt = self.build_thinking_prompt(&context);
let response = self.llm.complete(&prompt).await?;
let decision = self.parse_decision(&response)?;
Ok(decision)
}
pub async fn execute_task(&self, task: &str) -> Result<String> {
let prompt = format!(
"You are {}, {}. Your task is: {}\n\nResponse:",
self.agent.name,
self.agent.system_prompt(),
task
);
let response = self.llm.complete(&prompt).await?;
Ok(response.trim().to_string())
}
fn build_thinking_prompt(&self, context: &Context) -> String {
let mut prompt = format!(
"You are {}, {}. ",
self.agent.name,
self.agent.system_prompt()
);
if let Some(org_info) = &context.organization_info {
prompt.push_str(&format!("Organization info: {}\n", org_info));
}
if let Some(task) = &context.current_task {
prompt.push_str(&format!("Your current task is: {}\n", task));
}
if !context.unread_messages.is_empty() {
prompt.push_str("\nRecent messages:\n");
for msg in &context.unread_messages {
let target_desc = match &msg.to {
MessageTarget::Direct(id) => format!("Direct to {}", id),
MessageTarget::Group(id) => format!("Group {}", id),
};
prompt.push_str(&format!(
"- From {}: {} (to: {})\n",
msg.from, msg.content, target_desc
));
}
}
prompt.push_str("\nBased on this information, what is your decision or action? Respond in JSON format with fields: 'action' (one of 'send_message', 'create_group', 'execute_task', 'wait'), 'target' (for messages), 'content' (for messages), 'group_name' (for groups), 'members' (for groups), 'task' (for execute_task).");
prompt
}
fn parse_decision(&self, response: &str) -> Result<Decision> {
Ok(Decision::ExecuteTask {
task: response.to_string(),
})
}
}
enum ReActStepResult {
Action {
tool_calls: Vec<ToolCall>,
observation: String,
},
FinalDecision(Decision),
}
#[derive(Debug, Clone, Default)]
pub struct Context {
pub unread_messages: Vec<Message>,
pub current_task: Option<String>,
pub organization_info: Option<String>,
pub observations: Vec<String>,
}
impl Context {
pub fn with_messages(mut self, messages: Vec<Message>) -> Self {
self.unread_messages = messages;
self
}
pub fn with_task(mut self, task: impl Into<String>) -> Self {
self.current_task = Some(task.into());
self
}
pub fn with_observation(mut self, observation: impl Into<String>) -> Self {
self.observations.push(observation.into());
self
}
pub fn with_observations(mut self, observations: Vec<String>) -> Self {
self.observations.extend(observations);
self
}
}
#[derive(Debug, Clone)]
pub enum Decision {
SendMessage {
target: MessageTarget,
content: String,
},
CreateGroup {
name: String,
members: Vec<String>,
},
ExecuteTask {
task: String,
},
Wait,
Error(String),
}