chat agent
This commit is contained in:
parent
b9c6c831c1
commit
b44e4ed6f9
11 changed files with 626 additions and 16 deletions
130
src/server/chat.rs
Normal file
130
src/server/chat.rs
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
use crate::domain::agent::api::{ChatRequest, ChatResponse, Message};
|
||||
use crate::error::AppError;
|
||||
use crate::server::AppState;
|
||||
use axum::{Json, extract::State};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ChatPayload {
|
||||
pub messages: Vec<Message>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ChatResult {
|
||||
pub message: Message,
|
||||
}
|
||||
|
||||
pub async fn chat_handler(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(payload): Json<ChatPayload>,
|
||||
) -> Result<Json<ChatResult>, AppError> {
|
||||
let msg_count = payload.messages.len();
|
||||
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 tools = crate::domain::agent::tools::get_tools();
|
||||
|
||||
let max_turns = 10;
|
||||
let mut turns = 0;
|
||||
|
||||
loop {
|
||||
turns += 1;
|
||||
if turns > max_turns {
|
||||
return Err(AppError::Internal("Chat turn limit exceeded".into()));
|
||||
}
|
||||
|
||||
let request = ChatRequest {
|
||||
model: "big-pickle".to_string(),
|
||||
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_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
|
||||
tracing::info!("Chat completion successful");
|
||||
return Ok(Json(ChatResult {
|
||||
message: assistant_message,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
pub mod auth;
|
||||
pub mod chat;
|
||||
pub mod tasks;
|
||||
|
||||
use axum::{
|
||||
|
|
@ -120,6 +121,7 @@ fn build_app(state: Arc<AppState>, config: &crate::config::Config) -> Router {
|
|||
.route("/api/auth/callback", get(auth::auth_callback))
|
||||
.route("/api/auth/refresh", post(auth::auth_refresh))
|
||||
.route("/api/auth/logout", post(auth::auth_logout))
|
||||
.route("/api/chat", post(chat::chat_handler))
|
||||
.layer(cors)
|
||||
.layer(tower_http::set_header::SetResponseHeaderLayer::overriding(
|
||||
axum::http::header::CONTENT_SECURITY_POLICY,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue