This commit is contained in:
parent
46a061a61f
commit
247afd11cd
2 changed files with 271 additions and 5 deletions
215
src/services/llm-email.md
Normal file
215
src/services/llm-email.md
Normal file
|
@ -0,0 +1,215 @@
|
|||
use actix_web::{web, HttpResponse, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct ChatRequest {
|
||||
input: String,
|
||||
context: Option<AppContext>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct AppContext {
|
||||
view_type: Option<String>,
|
||||
email_context: Option<EmailContext>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct EmailContext {
|
||||
id: String,
|
||||
subject: String,
|
||||
labels: Vec<String>,
|
||||
from: Option<String>,
|
||||
to: Option<Vec<String>>,
|
||||
body: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct ChatResponse {
|
||||
response: String,
|
||||
tool_calls: Option<Vec<ToolCall>>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct ToolCall {
|
||||
tool_name: String,
|
||||
parameters: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct ToolDefinition {
|
||||
name: String,
|
||||
description: String,
|
||||
parameters: serde_json::Value,
|
||||
}
|
||||
|
||||
#[actix_web::post("/chat")]
|
||||
pub async fn chat(
|
||||
web::Json(request): web::Json<ChatRequest>,
|
||||
state: web::Data<AppState>,
|
||||
) -> Result<HttpResponse> {
|
||||
let azure_config = from_config(&state.config.clone().unwrap().ai);
|
||||
let open_ai = OpenAI::new(azure_config);
|
||||
|
||||
// Define available tools based on context
|
||||
let tools = get_available_tools(&request.context);
|
||||
|
||||
// Build the prompt with context and available tools
|
||||
let system_prompt = build_system_prompt(&request.context, &tools);
|
||||
let user_message = format!("{}\n\nUser input: {}", system_prompt, request.input);
|
||||
|
||||
let response = match open_ai.invoke(&user_message).await {
|
||||
Ok(res) => res,
|
||||
Err(err) => {
|
||||
eprintln!("Error invoking API: {}", err);
|
||||
return Err(actix_web::error::ErrorInternalServerError(
|
||||
"Failed to invoke OpenAI API",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Parse the response for tool calls
|
||||
let tool_calls = parse_tool_calls(&response);
|
||||
|
||||
let chat_response = ChatResponse {
|
||||
response,
|
||||
tool_calls,
|
||||
};
|
||||
|
||||
Ok(HttpResponse::Ok().json(chat_response))
|
||||
}
|
||||
|
||||
fn get_available_tools(context: &Option<AppContext>) -> Vec<ToolDefinition> {
|
||||
let mut tools = Vec::new();
|
||||
|
||||
if let Some(ctx) = context {
|
||||
if let Some(view_type) = &ctx.view_type {
|
||||
match view_type.as_str() {
|
||||
"email" => {
|
||||
tools.push(ToolDefinition {
|
||||
name: "replyEmail".to_string(),
|
||||
description: "Reply to the current email with generated content".to_string(),
|
||||
parameters: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "The reply content to send"
|
||||
}
|
||||
},
|
||||
"required": ["content"]
|
||||
}),
|
||||
});
|
||||
|
||||
tools.push(ToolDefinition {
|
||||
name: "forwardEmail".to_string(),
|
||||
description: "Forward the current email to specified recipients".to_string(),
|
||||
parameters: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"recipients": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Email addresses to forward to"
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "Additional message to include"
|
||||
}
|
||||
},
|
||||
"required": ["recipients"]
|
||||
}),
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tools
|
||||
}
|
||||
|
||||
fn build_system_prompt(context: &Option<AppContext>, tools: &[ToolDefinition]) -> String {
|
||||
let mut prompt = String::new();
|
||||
|
||||
if let Some(ctx) = context {
|
||||
if let Some(view_type) = &ctx.view_type {
|
||||
match view_type.as_str() {
|
||||
"email" => {
|
||||
if let Some(email_ctx) = &ctx.email_context {
|
||||
prompt.push_str(&format!(
|
||||
"You are an email assistant. Current email context:\n\
|
||||
Subject: {}\n\
|
||||
ID: {}\n\
|
||||
Labels: {:?}\n\n",
|
||||
email_ctx.subject, email_ctx.id, email_ctx.labels
|
||||
));
|
||||
|
||||
if let Some(from) = &email_ctx.from {
|
||||
prompt.push_str(&format!("From: {}\n", from));
|
||||
}
|
||||
|
||||
if let Some(body) = &email_ctx.body {
|
||||
prompt.push_str(&format!("Body: {}\n", body));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !tools.is_empty() {
|
||||
prompt.push_str("\nAvailable tools:\n");
|
||||
for tool in tools {
|
||||
prompt.push_str(&format!(
|
||||
"- {}: {}\n Parameters: {}\n\n",
|
||||
tool.name, tool.description, tool.parameters
|
||||
));
|
||||
}
|
||||
|
||||
prompt.push_str(
|
||||
"If you need to use a tool, respond with:\n\
|
||||
TOOL_CALL: tool_name\n\
|
||||
PARAMETERS: {json_parameters}\n\
|
||||
RESPONSE: your_response_text\n\n\
|
||||
Otherwise, just provide a normal response.\n"
|
||||
);
|
||||
}
|
||||
|
||||
prompt
|
||||
}
|
||||
|
||||
fn parse_tool_calls(response: &str) -> Option<Vec<ToolCall>> {
|
||||
if !response.contains("TOOL_CALL:") {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut tool_calls = Vec::new();
|
||||
let lines: Vec<&str> = response.lines().collect();
|
||||
|
||||
let mut i = 0;
|
||||
while i < lines.len() {
|
||||
if lines[i].starts_with("TOOL_CALL:") {
|
||||
let tool_name = lines[i].replace("TOOL_CALL:", "").trim().to_string();
|
||||
|
||||
// Look for parameters in the next line
|
||||
if i + 1 < lines.len() && lines[i + 1].starts_with("PARAMETERS:") {
|
||||
let params_str = lines[i + 1].replace("PARAMETERS:", "").trim();
|
||||
|
||||
if let Ok(parameters) = serde_json::from_str::<serde_json::Value>(params_str) {
|
||||
tool_calls.push(ToolCall {
|
||||
tool_name,
|
||||
parameters,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
if tool_calls.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(tool_calls)
|
||||
}
|
||||
}
|
|
@ -29,18 +29,56 @@ pub fn from_config(config: &AIConfig) -> AzureConfig {
|
|||
.with_deployment_id(&config.instance)
|
||||
}
|
||||
|
||||
use serde_json::json;
|
||||
#[derive(serde::Deserialize)]
|
||||
struct ChatRequest {
|
||||
input: String,
|
||||
}#[actix_web::post("/chat")]
|
||||
context: String,
|
||||
|
||||
}
|
||||
#[derive(serde::Serialize)]
|
||||
struct ChatResponse {
|
||||
text: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
action: Option<ChatAction>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
#[serde(tag = "type", content = "content")]
|
||||
enum ChatAction {
|
||||
ReplyEmail { content: String },
|
||||
// Add other action variants here as needed
|
||||
}
|
||||
|
||||
#[actix_web::post("/chat")]
|
||||
pub async fn chat(
|
||||
web::Json(request): web::Json<ChatRequest>,
|
||||
web::Json(request): web::Json<String>,
|
||||
state: web::Data<AppState>,
|
||||
) -> Result<impl Responder, actix_web::Error> {
|
||||
let azure_config = from_config(&state.config.clone().unwrap().ai);
|
||||
let open_ai = OpenAI::new(azure_config);
|
||||
|
||||
let response = match open_ai.invoke(&request.input).await {
|
||||
// Parse the context JSON
|
||||
let context: serde_json::Value = match serde_json::from_str(&request.context) {
|
||||
Ok(ctx) => ctx,
|
||||
Err(_) => serde_json::json!({})
|
||||
};
|
||||
|
||||
// Check view type and prepare appropriate prompt
|
||||
let view_type = context.get("viewType").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let (prompt, might_trigger_action) = match view_type {
|
||||
"email" => (
|
||||
format!(
|
||||
"Respond to this email: {}. Keep it professional and concise. \
|
||||
If the email requires a response, provide one in the 'replyEmail' action format.",
|
||||
request.input
|
||||
),
|
||||
true,
|
||||
),
|
||||
_ => (request.input, false),
|
||||
};
|
||||
|
||||
let response_text = match open_ai.invoke(&prompt).await {
|
||||
Ok(res) => res,
|
||||
Err(err) => {
|
||||
eprintln!("Error invoking API: {}", err);
|
||||
|
@ -49,9 +87,22 @@ pub async fn chat(
|
|||
));
|
||||
}
|
||||
};
|
||||
Ok(HttpResponse::Ok().body(response))
|
||||
}
|
||||
|
||||
// Prepare response with potential action
|
||||
let mut chat_response = ChatResponse {
|
||||
text: response_text.clone(),
|
||||
action: None,
|
||||
};
|
||||
|
||||
// If in email view and the response looks like an email reply, add action
|
||||
if might_trigger_action && view_type == "email" {
|
||||
chat_response.action = Some(ChatAction::ReplyEmail {
|
||||
content: response_text,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(HttpResponse::Ok().json(chat_response))
|
||||
}
|
||||
|
||||
#[actix_web::post("/stream")]
|
||||
pub async fn chat_stream(
|
||||
|
|
Loading…
Add table
Reference in a new issue