idk
This commit is contained in:
parent
bab956387c
commit
beab32894b
3 changed files with 128 additions and 132 deletions
|
|
@ -399,7 +399,7 @@ function renderChat() {
|
||||||
chatMessagesEl.innerHTML = state.chatMessages
|
chatMessagesEl.innerHTML = state.chatMessages
|
||||||
.map(msg => `
|
.map(msg => `
|
||||||
<div class="chat-message ${msg.role}">
|
<div class="chat-message ${msg.role}">
|
||||||
${escapeHtml(msg.content)}
|
${DOMPurify.sanitize(marked.parse(msg.content || ''))}
|
||||||
</div>
|
</div>
|
||||||
`)
|
`)
|
||||||
.join('');
|
.join('');
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ pub struct Agent {
|
||||||
url: String,
|
url: String,
|
||||||
zen_api_key: Option<String>,
|
zen_api_key: Option<String>,
|
||||||
tavily_api_key: Option<String>,
|
tavily_api_key: Option<String>,
|
||||||
messages: Vec<Message>,
|
pub messages: Vec<Message>,
|
||||||
tools: Option<Vec<Tool>>,
|
tools: Option<Vec<Tool>>,
|
||||||
logs: String,
|
logs: String,
|
||||||
answer: Option<String>,
|
answer: Option<String>,
|
||||||
|
|
@ -50,10 +50,19 @@ impl Agent {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
Self::with_messages(db, zen_api_key, tavily_api_key, messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_messages(
|
||||||
|
db: DatabaseConnection,
|
||||||
|
zen_api_key: Option<String>,
|
||||||
|
tavily_api_key: Option<String>,
|
||||||
|
messages: Vec<Message>,
|
||||||
|
) -> AppResult<Self> {
|
||||||
let tools = Some(tools::get_tools());
|
let tools = Some(tools::get_tools());
|
||||||
|
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
.timeout(std::time::Duration::from_secs(60))
|
.timeout(std::time::Duration::from_secs(120))
|
||||||
.build()
|
.build()
|
||||||
.map_err(|e| AppError::Internal(format!("Failed to build HTTP client: {}", e)))?;
|
.map_err(|e| AppError::Internal(format!("Failed to build HTTP client: {}", e)))?;
|
||||||
|
|
||||||
|
|
@ -105,6 +114,35 @@ impl Agent {
|
||||||
turns, current_role
|
turns, current_role
|
||||||
));
|
));
|
||||||
|
|
||||||
|
let assistant_message = self.execute_turn().await?;
|
||||||
|
|
||||||
|
if let Some(tool_calls) = &assistant_message.tool_calls {
|
||||||
|
if tool_calls.iter().any(|tc| tc.function.name == "answer") {
|
||||||
|
finished = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.answer.is_some() {
|
||||||
|
finished = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.log("\n--- Execution Finished ---");
|
||||||
|
Ok((self.logs.clone(), self.answer.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn execute_turn(&mut self) -> AppResult<Message> {
|
||||||
|
let max_sub_turns = 10;
|
||||||
|
let mut sub_turns = 0;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
sub_turns += 1;
|
||||||
|
if sub_turns > max_sub_turns {
|
||||||
|
return Err(AppError::Internal(
|
||||||
|
"Interaction cycle turn limit exceeded".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let chat_response = self.call_llm().await?;
|
let chat_response = self.call_llm().await?;
|
||||||
let assistant_message = chat_response
|
let assistant_message = chat_response
|
||||||
.choices
|
.choices
|
||||||
|
|
@ -121,20 +159,24 @@ impl Agent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(tool_calls) = assistant_message.tool_calls {
|
if let Some(tool_calls) = &assistant_message.tool_calls {
|
||||||
|
let mut is_final_cycle = false;
|
||||||
|
let mut final_answer = None;
|
||||||
|
|
||||||
for tool_call in tool_calls {
|
for tool_call in tool_calls {
|
||||||
self.log(&format!("Calling tool: {}", tool_call.function.name));
|
self.log(&format!("Calling tool: {}", tool_call.function.name));
|
||||||
|
|
||||||
let (tool_message, is_final, tool_answer) =
|
let (tool_message, is_final, tool_answer) =
|
||||||
tools::handle_tool_call(&tool_call, &self.tavily_api_key, &self.db)
|
tools::handle_tool_call(tool_call, &self.tavily_api_key, &self.db)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
AppError::Internal(format!("Tool execution failed: {}", e))
|
AppError::Internal(format!("Tool execution failed: {}", e))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if let Some(ans) = tool_answer {
|
if let Some(ans) = tool_answer {
|
||||||
self.answer = Some(ans);
|
self.answer = Some(ans.clone());
|
||||||
self.log("Task marked as finished by tool.");
|
final_answer = Some(ans);
|
||||||
|
self.log("Interaction marked as finished by tool.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(content) = &tool_message.content {
|
if let Some(content) = &tool_message.content {
|
||||||
|
|
@ -143,20 +185,24 @@ impl Agent {
|
||||||
|
|
||||||
self.messages.push(tool_message);
|
self.messages.push(tool_message);
|
||||||
if is_final {
|
if is_final {
|
||||||
finished = true;
|
is_final_cycle = true;
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if assistant_message.content.is_some() {
|
|
||||||
// If assistant just talked without tools, we might be stuck or finished.
|
|
||||||
// But typically we expect a 'finish' tool call.
|
|
||||||
self.log("Assistant responded without tool calls.");
|
|
||||||
// For now we continue unless the assistant explicitly uses a tool to finish,
|
|
||||||
// or we could add heuristic here if needed.
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.log("\n--- Execution Finished ---");
|
if is_final_cycle {
|
||||||
Ok((self.logs.clone(), self.answer.clone()))
|
return Ok(Message {
|
||||||
|
role: "assistant".to_string(),
|
||||||
|
content: final_answer.or(assistant_message.content),
|
||||||
|
tool_calls: None,
|
||||||
|
tool_call_id: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(assistant_message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn call_llm(&self) -> AppResult<ChatResponse> {
|
async fn call_llm(&self) -> AppResult<ChatResponse> {
|
||||||
|
|
@ -172,23 +218,46 @@ impl Agent {
|
||||||
request_builder = request_builder.header("Authorization", format!("Bearer {}", key));
|
request_builder = request_builder.header("Authorization", format!("Bearer {}", key));
|
||||||
}
|
}
|
||||||
|
|
||||||
let response = request_builder.send().await.map_err(AppError::Network)?;
|
let start = std::time::Instant::now();
|
||||||
|
let response = request_builder.send().await.map_err(|e| {
|
||||||
|
let duration = start.elapsed();
|
||||||
|
let is_timeout = e.is_timeout();
|
||||||
|
tracing::error!(
|
||||||
|
"Network error after {:?} during LLM call (Timeout: {}): {:?}",
|
||||||
|
duration,
|
||||||
|
is_timeout,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
AppError::Network(e)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let duration = start.elapsed();
|
||||||
|
tracing::info!("LLM request completed in {:?}", duration);
|
||||||
|
|
||||||
if !response.status().is_success() {
|
if !response.status().is_success() {
|
||||||
let status = response.status();
|
let status = response.status();
|
||||||
let error_text = response
|
let body_text = response
|
||||||
.text()
|
.text()
|
||||||
.await
|
.await
|
||||||
.unwrap_or_else(|_| "Unknown error".into());
|
.unwrap_or_else(|_| "Unknown body".into());
|
||||||
return Err(AppError::Internal(format!(
|
let err = format!("API request failed: {} - {}", status, body_text);
|
||||||
"API request failed: {} - {}",
|
tracing::error!("{}", err);
|
||||||
status, error_text
|
return Err(AppError::Internal(err));
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
response
|
let response_text = response.text().await.map_err(|e| {
|
||||||
.json()
|
let err = format!("Failed to read response text: {}", e);
|
||||||
.await
|
tracing::error!("{}", err);
|
||||||
.map_err(|e| AppError::Internal(format!("Failed to parse LLM response: {}", e)))
|
AppError::Internal(err)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
serde_json::from_str(&response_text).map_err(|e| {
|
||||||
|
let err = format!(
|
||||||
|
"Failed to parse LLM response: {} | Raw Body: {}",
|
||||||
|
e, response_text
|
||||||
|
);
|
||||||
|
tracing::error!("{}", err);
|
||||||
|
AppError::Internal(err)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::domain::agent::api::{ChatRequest, ChatResponse, Message};
|
use crate::domain::agent::api::Message;
|
||||||
use crate::error::AppError;
|
use crate::error::AppError;
|
||||||
use crate::server::AppState;
|
use crate::server::AppState;
|
||||||
use axum::{Json, extract::State};
|
use axum::{Json, extract::State};
|
||||||
|
|
@ -22,109 +22,36 @@ pub async fn chat_handler(
|
||||||
let msg_count = payload.messages.len();
|
let msg_count = payload.messages.len();
|
||||||
tracing::info!("Received chat request with {} messages", msg_count);
|
tracing::info!("Received chat request with {} messages", msg_count);
|
||||||
|
|
||||||
let client = reqwest::Client::builder()
|
|
||||||
.timeout(std::time::Duration::from_secs(60))
|
|
||||||
.build()
|
|
||||||
.map_err(|e| {
|
|
||||||
let err = format!("Failed to build HTTP client: {}", e);
|
|
||||||
tracing::error!("{}", err);
|
|
||||||
AppError::Internal(err)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let url = "https://opencode.ai/zen/v1/chat/completions";
|
|
||||||
let mut messages = payload.messages;
|
let mut messages = payload.messages;
|
||||||
let tools = crate::domain::agent::tools::get_tools();
|
|
||||||
|
|
||||||
let max_turns = 10;
|
// Inject current date awareness if not already present or as a fresh system message
|
||||||
let mut turns = 0;
|
let now = chrono::Local::now();
|
||||||
|
let date_str = now.format("%A, %B %e, %Y at %l:%M %P").to_string();
|
||||||
loop {
|
messages.insert(
|
||||||
turns += 1;
|
0,
|
||||||
if turns > max_turns {
|
Message {
|
||||||
return Err(AppError::Internal("Chat turn limit exceeded".into()));
|
role: "system".to_string(),
|
||||||
}
|
content: Some(format!(
|
||||||
|
"The current date and time is {}. Today is {}. You are in interactive chat mode.",
|
||||||
let request = ChatRequest {
|
date_str,
|
||||||
model: "big-pickle".to_string(),
|
now.format("%Y-%m-%d")
|
||||||
messages: messages.clone(),
|
)),
|
||||||
tools: Some(tools.clone()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut request_builder = client.post(url).json(&request);
|
|
||||||
|
|
||||||
if let Some(key) = &state.config.zen_api_key {
|
|
||||||
request_builder = request_builder.header("Authorization", format!("Bearer {}", key));
|
|
||||||
}
|
|
||||||
|
|
||||||
let response = request_builder.send().await.map_err(|e| {
|
|
||||||
tracing::error!("Network error during chat completion: {}", e);
|
|
||||||
AppError::Network(e)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if !response.status().is_success() {
|
|
||||||
let status = response.status();
|
|
||||||
let error_text = response
|
|
||||||
.text()
|
|
||||||
.await
|
|
||||||
.unwrap_or_else(|_| "Unknown error".into());
|
|
||||||
let err = format!("API request failed: {} - {}", status, error_text);
|
|
||||||
tracing::error!("{}", err);
|
|
||||||
return Err(AppError::Internal(err));
|
|
||||||
}
|
|
||||||
|
|
||||||
let chat_response: ChatResponse = response.json().await.map_err(|e| {
|
|
||||||
let err = format!("Failed to parse LLM response: {}", e);
|
|
||||||
tracing::error!("{}", err);
|
|
||||||
AppError::Internal(err)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let assistant_message = chat_response
|
|
||||||
.choices
|
|
||||||
.get(0)
|
|
||||||
.ok_or_else(|| {
|
|
||||||
let err = "Missing assistant response choices";
|
|
||||||
tracing::error!("{}", err);
|
|
||||||
AppError::Internal(err.into())
|
|
||||||
})?
|
|
||||||
.message
|
|
||||||
.clone();
|
|
||||||
|
|
||||||
messages.push(assistant_message.clone());
|
|
||||||
|
|
||||||
if let Some(tool_calls) = &assistant_message.tool_calls {
|
|
||||||
for tool_call in tool_calls {
|
|
||||||
tracing::info!("Chat agent calling tool: {}", tool_call.function.name);
|
|
||||||
let (tool_message, is_final, answer) =
|
|
||||||
crate::domain::agent::tools::handle_tool_call(
|
|
||||||
tool_call,
|
|
||||||
&state.config.tavily_api_key,
|
|
||||||
&state.db,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|e| AppError::Internal(format!("Tool execution failed: {}", e)))?;
|
|
||||||
|
|
||||||
messages.push(tool_message.clone());
|
|
||||||
|
|
||||||
if is_final {
|
|
||||||
tracing::info!("Chat agent finished via tool");
|
|
||||||
return Ok(Json(ChatResult {
|
|
||||||
message: Message {
|
|
||||||
role: "assistant".to_string(),
|
|
||||||
content: answer.or(tool_message.content),
|
|
||||||
tool_calls: None,
|
tool_calls: None,
|
||||||
tool_call_id: None,
|
tool_call_id: None,
|
||||||
},
|
},
|
||||||
}));
|
);
|
||||||
}
|
|
||||||
}
|
|
||||||
// After tool calls, we loop back to get another assistant response
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no tool calls, it's a final response for this turn
|
let mut agent = crate::domain::agent::Agent::with_messages(
|
||||||
tracing::info!("Chat completion successful");
|
state.db.clone(),
|
||||||
return Ok(Json(ChatResult {
|
state.config.zen_api_key.clone(),
|
||||||
|
state.config.tavily_api_key.clone(),
|
||||||
|
messages,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
tracing::info!("Starting interactive agent turn");
|
||||||
|
let assistant_message = agent.execute_turn().await?;
|
||||||
|
|
||||||
|
Ok(Json(ChatResult {
|
||||||
message: assistant_message,
|
message: assistant_message,
|
||||||
}));
|
}))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue