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)
|
.with_deployment_id(&config.instance)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use serde_json::json;
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
struct ChatRequest {
|
struct ChatRequest {
|
||||||
input: String,
|
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(
|
pub async fn chat(
|
||||||
web::Json(request): web::Json<ChatRequest>,
|
web::Json(request): web::Json<String>,
|
||||||
state: web::Data<AppState>,
|
state: web::Data<AppState>,
|
||||||
) -> Result<impl Responder, actix_web::Error> {
|
) -> Result<impl Responder, actix_web::Error> {
|
||||||
let azure_config = from_config(&state.config.clone().unwrap().ai);
|
let azure_config = from_config(&state.config.clone().unwrap().ai);
|
||||||
let open_ai = OpenAI::new(azure_config);
|
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,
|
Ok(res) => res,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("Error invoking API: {}", 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")]
|
#[actix_web::post("/stream")]
|
||||||
pub async fn chat_stream(
|
pub async fn chat_stream(
|
||||||
|
|
Loading…
Add table
Reference in a new issue