refactor(agent): remove deprecated chat modules
Delete deprecated chat components: - agent_profile, context, message_builder - nonstreaming_execution, session_recording These are superseded by the consolidated chat module.
This commit is contained in:
parent
d3de12717d
commit
5827d561db
@ -1,433 +0,0 @@
|
|||||||
use crate::chat::{AgentExecutionProfile, AgentRole};
|
|
||||||
|
|
||||||
/// Tools available to every agent role (shared baseline).
|
|
||||||
fn shared_tools() -> Vec<String> {
|
|
||||||
vec![
|
|
||||||
// Conversation management
|
|
||||||
"chat_generate_title".into(),
|
|
||||||
// File parsing / search
|
|
||||||
"git_grep".into(),
|
|
||||||
"read_csv".into(),
|
|
||||||
"read_json".into(),
|
|
||||||
"read_sql".into(),
|
|
||||||
"read_markdown".into(),
|
|
||||||
// File content retrieval
|
|
||||||
"git_file_content".into(),
|
|
||||||
"git_blob_get".into(),
|
|
||||||
"git_blob_content".into(),
|
|
||||||
"git_status".into(),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Researcher-specific tools (search & discovery).
|
|
||||||
fn researcher_tools() -> Vec<String> {
|
|
||||||
vec![
|
|
||||||
// Search
|
|
||||||
"git_search_commits".into(),
|
|
||||||
"repo_search".into(),
|
|
||||||
"repo_doc_search".into(),
|
|
||||||
"project_bing_search".into(),
|
|
||||||
"project_arxiv_search".into(),
|
|
||||||
"project_curl".into(),
|
|
||||||
"curl_exec".into(),
|
|
||||||
// Index / overview
|
|
||||||
"repo_overview".into(),
|
|
||||||
"repo_readme".into(),
|
|
||||||
"repo_file_tree".into(),
|
|
||||||
"repo_doc_index".into(),
|
|
||||||
"repo_doc_read".into(),
|
|
||||||
// Lists
|
|
||||||
"project_list_repos".into(),
|
|
||||||
"project_list_members".into(),
|
|
||||||
"project_list_issues".into(),
|
|
||||||
"project_list_labels".into(),
|
|
||||||
"project_list_boards".into(),
|
|
||||||
// Log / history
|
|
||||||
"git_log".into(),
|
|
||||||
"git_reflog".into(),
|
|
||||||
"git_graph".into(),
|
|
||||||
"git_commit_info".into(),
|
|
||||||
"repo_commit_log".into(),
|
|
||||||
"git_ref_list".into(),
|
|
||||||
"git_ref_info".into(),
|
|
||||||
"git_lfs_summary".into(),
|
|
||||||
"git_lfs_scan_tree".into(),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Analyst-specific tools (deep analysis & explanation).
|
|
||||||
fn analyst_tools() -> Vec<String> {
|
|
||||||
vec![
|
|
||||||
// Deep inspection
|
|
||||||
"git_show".into(),
|
|
||||||
"git_diff".into(),
|
|
||||||
"git_diff_stats".into(),
|
|
||||||
"git_blame".into(),
|
|
||||||
// Structural analysis
|
|
||||||
"repo_languages".into(),
|
|
||||||
"repo_dependencies".into(),
|
|
||||||
"repo_diff_summary".into(),
|
|
||||||
"repo_contributors".into(),
|
|
||||||
// Branch comparison
|
|
||||||
"git_branch_list".into(),
|
|
||||||
"git_branch_info".into(),
|
|
||||||
"git_branch_diff".into(),
|
|
||||||
"git_merge_analysis".into(),
|
|
||||||
"git_ref_list".into(),
|
|
||||||
"git_ref_info".into(),
|
|
||||||
// Project data
|
|
||||||
"project_list_issues".into(),
|
|
||||||
"project_list_repos".into(),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reviewer-specific tools (evaluation & risk detection).
|
|
||||||
fn reviewer_tools() -> Vec<String> {
|
|
||||||
vec![
|
|
||||||
// Change inspection
|
|
||||||
"git_diff".into(),
|
|
||||||
"git_diff_stats".into(),
|
|
||||||
"git_blame".into(),
|
|
||||||
// Merge status
|
|
||||||
"git_branches_merged".into(),
|
|
||||||
"git_branch_info".into(),
|
|
||||||
"git_merge_analysis".into(),
|
|
||||||
// Tracking
|
|
||||||
"project_list_issues".into(),
|
|
||||||
"project_update_issue".into(),
|
|
||||||
// Boards
|
|
||||||
"project_list_boards".into(),
|
|
||||||
"project_update_board_card".into(),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Architect-specific tools (system design & dependency mapping).
|
|
||||||
fn architect_tools() -> Vec<String> {
|
|
||||||
vec![
|
|
||||||
// Repository structure
|
|
||||||
"repo_overview".into(),
|
|
||||||
"repo_readme".into(),
|
|
||||||
"repo_file_tree".into(),
|
|
||||||
"repo_languages".into(),
|
|
||||||
"repo_dependencies".into(),
|
|
||||||
"repo_test_discovery".into(),
|
|
||||||
// Change and branch context
|
|
||||||
"repo_diff_summary".into(),
|
|
||||||
"git_branch_list".into(),
|
|
||||||
"git_branch_info".into(),
|
|
||||||
"git_branch_diff".into(),
|
|
||||||
"git_merge_analysis".into(),
|
|
||||||
"git_ref_list".into(),
|
|
||||||
// Project context
|
|
||||||
"project_list_repos".into(),
|
|
||||||
"project_list_issues".into(),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Debugger-specific tools (root-cause analysis & history tracing).
|
|
||||||
fn debugger_tools() -> Vec<String> {
|
|
||||||
vec![
|
|
||||||
// Failure and change inspection
|
|
||||||
"git_show".into(),
|
|
||||||
"git_diff".into(),
|
|
||||||
"git_diff_stats".into(),
|
|
||||||
"git_blame".into(),
|
|
||||||
"git_log".into(),
|
|
||||||
"git_commit_info".into(),
|
|
||||||
"git_ref_info".into(),
|
|
||||||
// Structural clues
|
|
||||||
"repo_file_tree".into(),
|
|
||||||
"repo_dependencies".into(),
|
|
||||||
"repo_test_discovery".into(),
|
|
||||||
"repo_doc_search".into(),
|
|
||||||
// Issue context
|
|
||||||
"project_list_issues".into(),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Implementer-specific tools (implementation planning & code navigation).
|
|
||||||
fn implementer_tools() -> Vec<String> {
|
|
||||||
vec![
|
|
||||||
// Code and documentation context
|
|
||||||
"repo_overview".into(),
|
|
||||||
"repo_readme".into(),
|
|
||||||
"repo_file_tree".into(),
|
|
||||||
"repo_doc_index".into(),
|
|
||||||
"repo_doc_read".into(),
|
|
||||||
"repo_dependencies".into(),
|
|
||||||
"repo_test_discovery".into(),
|
|
||||||
// Current change context
|
|
||||||
"git_diff".into(),
|
|
||||||
"git_diff_stats".into(),
|
|
||||||
"git_show".into(),
|
|
||||||
"git_branch_info".into(),
|
|
||||||
// Project context
|
|
||||||
"project_list_issues".into(),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Tester-specific tools (coverage, regression, and validation planning).
|
|
||||||
fn tester_tools() -> Vec<String> {
|
|
||||||
vec![
|
|
||||||
// Test discovery and changed surface
|
|
||||||
"git_grep".into(),
|
|
||||||
"git_diff".into(),
|
|
||||||
"git_diff_stats".into(),
|
|
||||||
"git_show".into(),
|
|
||||||
"repo_file_tree".into(),
|
|
||||||
"repo_dependencies".into(),
|
|
||||||
"repo_test_discovery".into(),
|
|
||||||
// Project context
|
|
||||||
"project_list_issues".into(),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Security-specific tools (threat modeling & sensitive-code review).
|
|
||||||
fn security_tools() -> Vec<String> {
|
|
||||||
vec![
|
|
||||||
// Sensitive pattern discovery
|
|
||||||
"git_grep".into(),
|
|
||||||
"git_diff".into(),
|
|
||||||
"git_diff_stats".into(),
|
|
||||||
"git_blame".into(),
|
|
||||||
"git_log".into(),
|
|
||||||
// Dependency and surface review
|
|
||||||
"repo_file_tree".into(),
|
|
||||||
"repo_dependencies".into(),
|
|
||||||
"repo_doc_search".into(),
|
|
||||||
"git_lfs_summary".into(),
|
|
||||||
"git_lfs_pointer_info".into(),
|
|
||||||
"git_lfs_object_info".into(),
|
|
||||||
// Issue context
|
|
||||||
"project_list_issues".into(),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Supervisor-specific tools (delegation & synthesis).
|
|
||||||
fn supervisor_tools() -> Vec<String> {
|
|
||||||
vec![
|
|
||||||
// Delegation
|
|
||||||
"call_sub_agent".into(),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the complete tool set for a given agent role (shared + role-specific).
|
|
||||||
pub fn tools_for_role(role: &AgentRole) -> Vec<String> {
|
|
||||||
let mut tools = shared_tools();
|
|
||||||
match role {
|
|
||||||
AgentRole::Researcher => tools.extend(researcher_tools()),
|
|
||||||
AgentRole::Analyst => tools.extend(analyst_tools()),
|
|
||||||
AgentRole::Reviewer => tools.extend(reviewer_tools()),
|
|
||||||
AgentRole::Architect => tools.extend(architect_tools()),
|
|
||||||
AgentRole::Debugger => tools.extend(debugger_tools()),
|
|
||||||
AgentRole::Implementer => tools.extend(implementer_tools()),
|
|
||||||
AgentRole::Tester => tools.extend(tester_tools()),
|
|
||||||
AgentRole::Security => tools.extend(security_tools()),
|
|
||||||
AgentRole::Supervisor => tools.extend(supervisor_tools()),
|
|
||||||
AgentRole::Default => {} // Default role gets only shared tools
|
|
||||||
}
|
|
||||||
tools
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn profile_for_role_name(role: &str) -> AgentExecutionProfile {
|
|
||||||
match role {
|
|
||||||
"researcher" => researcher_profile(),
|
|
||||||
"analyst" => analyst_profile(),
|
|
||||||
"reviewer" => reviewer_profile(),
|
|
||||||
"architect" => architect_profile(),
|
|
||||||
"debugger" => debugger_profile(),
|
|
||||||
"implementer" => implementer_profile(),
|
|
||||||
"tester" => tester_profile(),
|
|
||||||
"security" => security_profile(),
|
|
||||||
_ => researcher_profile(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn supervisor_profile() -> AgentExecutionProfile {
|
|
||||||
AgentExecutionProfile {
|
|
||||||
role: AgentRole::Supervisor,
|
|
||||||
system_prompt: Some(
|
|
||||||
"You are the supervisor agent. You coordinate specialist sub-agents to produce the best answer for the user.\n\
|
|
||||||
\n\
|
|
||||||
## Delegation Strategy\n\
|
|
||||||
- Use the `call_sub_agent` tool to delegate tasks to specialist agents.\n\
|
|
||||||
- Available roles:\n\
|
|
||||||
- **researcher**: Gathers concrete facts, evidence, and data from tools and context. Best for finding information, searching code, and discovering evidence.\n\
|
|
||||||
- **analyst**: Builds coherent explanations, highlights causal links, edge cases, and tradeoffs. Best for explaining findings and reasoning about implications.\n\
|
|
||||||
- **reviewer**: Stress-tests proposals, identifies contradictions, missing assumptions, regressions, and risks. Best for quality checks and risk assessment.\n\
|
|
||||||
- **architect**: Maps systems, boundaries, dependencies, and long-term design tradeoffs. Best for architecture decisions and refactor strategy.\n\
|
|
||||||
- **debugger**: Finds likely root causes, reproduction gaps, and suspect changes. Best for failures, regressions, and confusing behavior.\n\
|
|
||||||
- **implementer**: Turns requirements into concrete implementation steps, affected files, and integration concerns. Best for execution planning.\n\
|
|
||||||
- **tester**: Designs validation strategy, regression coverage, and edge-case test plans. Best for test planning and release confidence.\n\
|
|
||||||
- **security**: Reviews auth, data exposure, injection, dependency, and abuse risks. Best for threat modeling and sensitive changes.\n\
|
|
||||||
- Provide a clear, focused task description for each sub-agent.\n\
|
|
||||||
- You may call multiple sub-agents in sequence (call one, review its output, then decide to call another).\n\
|
|
||||||
- You may also call the same role twice with different tasks if needed.\n\
|
|
||||||
\n\
|
|
||||||
## Decision Guide\n\
|
|
||||||
- Simple factual questions: call researcher only.\n\
|
|
||||||
- Questions requiring explanation: call researcher then analyst.\n\
|
|
||||||
- Design/architecture reviews: call researcher, architect, then reviewer.\n\
|
|
||||||
- Bug or regression diagnosis: call debugger, then tester if validation is needed.\n\
|
|
||||||
- Implementation requests: call implementer, then reviewer for risk checks.\n\
|
|
||||||
- Security-sensitive changes: call security, then reviewer.\n\
|
|
||||||
- If a sub-agent's output is insufficient, call another sub-agent for clarification.\n\
|
|
||||||
\n\
|
|
||||||
## Output Rules\n\
|
|
||||||
- After gathering all sub-agent outputs, synthesize them into one final answer.\n\
|
|
||||||
- Resolve conflicts between sub-agent outputs — prefer evidence over speculation.\n\
|
|
||||||
- Call out any remaining uncertainty explicitly.\n\
|
|
||||||
- Do not assume facts not present in sub-agent outputs.".to_string(),
|
|
||||||
),
|
|
||||||
temperature: Some(0.2),
|
|
||||||
max_tokens: Some(4000),
|
|
||||||
top_p: Some(1.0),
|
|
||||||
frequency_penalty: Some(0.0),
|
|
||||||
presence_penalty: Some(0.0),
|
|
||||||
max_tool_depth: Some(8),
|
|
||||||
allowed_tools: Some(tools_for_role(&AgentRole::Supervisor)),
|
|
||||||
disable_orchestration: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn researcher_profile() -> AgentExecutionProfile {
|
|
||||||
AgentExecutionProfile {
|
|
||||||
role: AgentRole::Researcher,
|
|
||||||
system_prompt: Some(
|
|
||||||
"You are the researcher agent. Your job is to gather concrete facts from available tools and context. Prefer direct evidence over inference. Return structured findings, relevant code or data references, and unresolved gaps.".to_string(),
|
|
||||||
),
|
|
||||||
temperature: Some(0.1),
|
|
||||||
max_tokens: Some(1800),
|
|
||||||
top_p: Some(1.0),
|
|
||||||
frequency_penalty: Some(0.0),
|
|
||||||
presence_penalty: Some(0.0),
|
|
||||||
max_tool_depth: Some(6),
|
|
||||||
allowed_tools: Some(tools_for_role(&AgentRole::Researcher)),
|
|
||||||
disable_orchestration: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn analyst_profile() -> AgentExecutionProfile {
|
|
||||||
AgentExecutionProfile {
|
|
||||||
role: AgentRole::Analyst,
|
|
||||||
system_prompt: Some(
|
|
||||||
"You are the analyst agent. Build a coherent explanation from the available evidence. Highlight causal links, edge cases, and tradeoffs. If evidence is weak, say so explicitly.".to_string(),
|
|
||||||
),
|
|
||||||
temperature: Some(0.2),
|
|
||||||
max_tokens: Some(1800),
|
|
||||||
top_p: Some(1.0),
|
|
||||||
frequency_penalty: Some(0.0),
|
|
||||||
presence_penalty: Some(0.0),
|
|
||||||
max_tool_depth: Some(4),
|
|
||||||
allowed_tools: Some(tools_for_role(&AgentRole::Analyst)),
|
|
||||||
disable_orchestration: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reviewer_profile() -> AgentExecutionProfile {
|
|
||||||
AgentExecutionProfile {
|
|
||||||
role: AgentRole::Reviewer,
|
|
||||||
system_prompt: Some(
|
|
||||||
"You are the reviewer agent. Stress-test the proposed answer. Look for contradictions, missing assumptions, regressions, and risks. Output only high-signal critiques and corrections.".to_string(),
|
|
||||||
),
|
|
||||||
temperature: Some(0.1),
|
|
||||||
max_tokens: Some(1600),
|
|
||||||
top_p: Some(1.0),
|
|
||||||
frequency_penalty: Some(0.0),
|
|
||||||
presence_penalty: Some(0.0),
|
|
||||||
max_tool_depth: Some(4),
|
|
||||||
allowed_tools: Some(tools_for_role(&AgentRole::Reviewer)),
|
|
||||||
disable_orchestration: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn architect_profile() -> AgentExecutionProfile {
|
|
||||||
AgentExecutionProfile {
|
|
||||||
role: AgentRole::Architect,
|
|
||||||
system_prompt: Some(
|
|
||||||
"You are the architect agent. Map system boundaries, dependencies, data flow, and design tradeoffs. Prefer practical architecture guidance tied to repository evidence. Call out migration risks and long-term maintainability concerns.".to_string(),
|
|
||||||
),
|
|
||||||
temperature: Some(0.2),
|
|
||||||
max_tokens: Some(2000),
|
|
||||||
top_p: Some(1.0),
|
|
||||||
frequency_penalty: Some(0.0),
|
|
||||||
presence_penalty: Some(0.0),
|
|
||||||
max_tool_depth: Some(5),
|
|
||||||
allowed_tools: Some(tools_for_role(&AgentRole::Architect)),
|
|
||||||
disable_orchestration: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn debugger_profile() -> AgentExecutionProfile {
|
|
||||||
AgentExecutionProfile {
|
|
||||||
role: AgentRole::Debugger,
|
|
||||||
system_prompt: Some(
|
|
||||||
"You are the debugger agent. Identify likely root causes, suspect files or commits, missing reproduction details, and the shortest validation path. Separate evidence from hypotheses and rank hypotheses by plausibility.".to_string(),
|
|
||||||
),
|
|
||||||
temperature: Some(0.1),
|
|
||||||
max_tokens: Some(1800),
|
|
||||||
top_p: Some(1.0),
|
|
||||||
frequency_penalty: Some(0.0),
|
|
||||||
presence_penalty: Some(0.0),
|
|
||||||
max_tool_depth: Some(6),
|
|
||||||
allowed_tools: Some(tools_for_role(&AgentRole::Debugger)),
|
|
||||||
disable_orchestration: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn implementer_profile() -> AgentExecutionProfile {
|
|
||||||
AgentExecutionProfile {
|
|
||||||
role: AgentRole::Implementer,
|
|
||||||
system_prompt: Some(
|
|
||||||
"You are the implementer agent. Convert requirements into a concrete execution plan: files to touch, sequencing, integration points, and risks. Keep recommendations actionable and avoid broad rewrites unless justified.".to_string(),
|
|
||||||
),
|
|
||||||
temperature: Some(0.15),
|
|
||||||
max_tokens: Some(1800),
|
|
||||||
top_p: Some(1.0),
|
|
||||||
frequency_penalty: Some(0.0),
|
|
||||||
presence_penalty: Some(0.0),
|
|
||||||
max_tool_depth: Some(5),
|
|
||||||
allowed_tools: Some(tools_for_role(&AgentRole::Implementer)),
|
|
||||||
disable_orchestration: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tester_profile() -> AgentExecutionProfile {
|
|
||||||
AgentExecutionProfile {
|
|
||||||
role: AgentRole::Tester,
|
|
||||||
system_prompt: Some(
|
|
||||||
"You are the tester agent. Design high-signal validation: unit, integration, regression, and edge-case coverage. Identify what must be tested, what can be skipped, and the fastest commands or checks to build confidence.".to_string(),
|
|
||||||
),
|
|
||||||
temperature: Some(0.1),
|
|
||||||
max_tokens: Some(1600),
|
|
||||||
top_p: Some(1.0),
|
|
||||||
frequency_penalty: Some(0.0),
|
|
||||||
presence_penalty: Some(0.0),
|
|
||||||
max_tool_depth: Some(4),
|
|
||||||
allowed_tools: Some(tools_for_role(&AgentRole::Tester)),
|
|
||||||
disable_orchestration: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn security_profile() -> AgentExecutionProfile {
|
|
||||||
AgentExecutionProfile {
|
|
||||||
role: AgentRole::Security,
|
|
||||||
system_prompt: Some(
|
|
||||||
"You are the security agent. Review authentication, authorization, data exposure, injection, dependency, secret-handling, and abuse-case risks. Prioritize exploitable issues and concrete mitigations over generic advice.".to_string(),
|
|
||||||
),
|
|
||||||
temperature: Some(0.1),
|
|
||||||
max_tokens: Some(1800),
|
|
||||||
top_p: Some(1.0),
|
|
||||||
frequency_penalty: Some(0.0),
|
|
||||||
presence_penalty: Some(0.0),
|
|
||||||
max_tool_depth: Some(5),
|
|
||||||
allowed_tools: Some(tools_for_role(&AgentRole::Security)),
|
|
||||||
disable_orchestration: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether to enable multi-agent delegation for this request.
|
|
||||||
/// Simplified from keyword-based gating: delegation is enabled when tools are available.
|
|
||||||
pub fn should_enable_delegation(_input: &str, tools_available: bool) -> bool {
|
|
||||||
tools_available
|
|
||||||
}
|
|
||||||
@ -1,185 +0,0 @@
|
|||||||
use crate::client::ChatRequestMessage;
|
|
||||||
use crate::compact::MessageSummary;
|
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use models::rooms::MessageSenderType;
|
|
||||||
use models::rooms::room_message::Model as RoomMessageModel;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use serde_json::Value;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
/// Sender type for AI context, supporting all roles in the chat.
|
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
||||||
pub enum AiContextSenderType {
|
|
||||||
/// Regular user message
|
|
||||||
User,
|
|
||||||
/// AI assistant message
|
|
||||||
Ai,
|
|
||||||
/// System message (e.g., summary, notification)
|
|
||||||
System,
|
|
||||||
/// Developer message (for system-level instructions)
|
|
||||||
Developer,
|
|
||||||
/// Tool call message
|
|
||||||
Function,
|
|
||||||
/// Tool result message
|
|
||||||
FunctionResult,
|
|
||||||
Webhook,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AiContextSenderType {
|
|
||||||
pub fn from_sender_type(sender_type: &models::rooms::MessageSenderType) -> Self {
|
|
||||||
match sender_type {
|
|
||||||
models::rooms::MessageSenderType::Ai => Self::Ai,
|
|
||||||
models::rooms::MessageSenderType::System => Self::System,
|
|
||||||
models::rooms::MessageSenderType::Tool => Self::FunctionResult,
|
|
||||||
MessageSenderType::User => Self::User,
|
|
||||||
MessageSenderType::Webhook => Self::Webhook,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Room message context for AI processing.
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
pub struct RoomMessageContext {
|
|
||||||
pub uid: Uuid,
|
|
||||||
pub sender_type: AiContextSenderType,
|
|
||||||
pub sender_uid: Option<Uuid>,
|
|
||||||
pub sender_name: Option<String>,
|
|
||||||
pub content: String,
|
|
||||||
pub content_type: models::rooms::MessageContentType,
|
|
||||||
pub send_at: DateTime<Utc>,
|
|
||||||
/// Tool call ID for FunctionResult messages, used to associate tool results with their calls.
|
|
||||||
pub tool_call_id: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RoomMessageContext {
|
|
||||||
pub fn from_model(model: &RoomMessageModel, sender_name: Option<String>) -> Self {
|
|
||||||
Self {
|
|
||||||
uid: model.id,
|
|
||||||
sender_type: AiContextSenderType::from_sender_type(&model.sender_type),
|
|
||||||
sender_uid: model.sender_id,
|
|
||||||
sender_name,
|
|
||||||
content: model.content.clone(),
|
|
||||||
content_type: model.content_type.clone(),
|
|
||||||
send_at: model.send_at,
|
|
||||||
tool_call_id: Self::extract_tool_call_id(&model.content),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn extract_tool_call_id(content: &str) -> Option<String> {
|
|
||||||
let content = content.trim();
|
|
||||||
if let Ok(v) = serde_json::from_str::<Value>(content) {
|
|
||||||
v.get("tool_call_id")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.map(|s| s.to_string())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_model_with_names(
|
|
||||||
model: &RoomMessageModel,
|
|
||||||
user_names: &HashMap<Uuid, String>,
|
|
||||||
) -> Self {
|
|
||||||
let sender_name = model
|
|
||||||
.sender_id
|
|
||||||
.and_then(|uid| user_names.get(&uid).cloned());
|
|
||||||
Self::from_model(model, sender_name)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_message(&self) -> ChatRequestMessage {
|
|
||||||
let name_str = self.sender_name.clone();
|
|
||||||
match self.sender_type {
|
|
||||||
AiContextSenderType::User => {
|
|
||||||
let mut msg = ChatRequestMessage::user(self.display_content());
|
|
||||||
if let Some(n) = name_str {
|
|
||||||
msg = msg.with_name(n);
|
|
||||||
}
|
|
||||||
msg
|
|
||||||
}
|
|
||||||
AiContextSenderType::Ai => {
|
|
||||||
let mut msg = ChatRequestMessage::assistant(Some(self.display_content()), None);
|
|
||||||
if let Some(n) = name_str {
|
|
||||||
msg = msg.with_name(n);
|
|
||||||
}
|
|
||||||
msg
|
|
||||||
}
|
|
||||||
AiContextSenderType::System => {
|
|
||||||
let mut msg = ChatRequestMessage::system(&self.display_content());
|
|
||||||
if let Some(n) = name_str {
|
|
||||||
msg = msg.with_name(n);
|
|
||||||
}
|
|
||||||
msg
|
|
||||||
}
|
|
||||||
AiContextSenderType::Developer => {
|
|
||||||
let mut msg = ChatRequestMessage::developer(&self.display_content());
|
|
||||||
if let Some(n) = name_str {
|
|
||||||
msg = msg.with_name(n);
|
|
||||||
}
|
|
||||||
msg
|
|
||||||
}
|
|
||||||
AiContextSenderType::Function => ChatRequestMessage::user(&self.content)
|
|
||||||
.with_name(self.sender_name.as_deref().unwrap_or("unknown")),
|
|
||||||
AiContextSenderType::FunctionResult => {
|
|
||||||
let id = self
|
|
||||||
.tool_call_id
|
|
||||||
.clone()
|
|
||||||
.unwrap_or_else(|| "unknown".to_string());
|
|
||||||
ChatRequestMessage::tool(id, self.display_content())
|
|
||||||
}
|
|
||||||
AiContextSenderType::Webhook => {
|
|
||||||
let mut msg = ChatRequestMessage::user(&self.content);
|
|
||||||
msg = msg.with_name(name_str.as_deref().unwrap_or("webhook"));
|
|
||||||
msg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn display_content(&self) -> String {
|
|
||||||
let mut content = self.content.trim().to_string();
|
|
||||||
if content.is_empty() {
|
|
||||||
content = match self.content_type {
|
|
||||||
models::rooms::MessageContentType::Text => "[empty]".to_string(),
|
|
||||||
models::rooms::MessageContentType::Image => "[image]".to_string(),
|
|
||||||
models::rooms::MessageContentType::Audio => "[audio]".to_string(),
|
|
||||||
models::rooms::MessageContentType::Video => "[video]".to_string(),
|
|
||||||
models::rooms::MessageContentType::File => "[file]".to_string(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(sender_name) = &self.sender_name {
|
|
||||||
content = format!("[{}] {}", sender_name, content);
|
|
||||||
}
|
|
||||||
|
|
||||||
content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&RoomMessageModel> for RoomMessageContext {
|
|
||||||
fn from(model: &RoomMessageModel) -> Self {
|
|
||||||
RoomMessageContext::from_model(model, None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<MessageSummary> for RoomMessageContext {
|
|
||||||
fn from(summary: MessageSummary) -> Self {
|
|
||||||
// Map MessageSenderType to AiContextSenderType
|
|
||||||
let sender_type = AiContextSenderType::from_sender_type(&summary.sender_type);
|
|
||||||
// For FunctionResult (tool results), ensure tool_call_id is set
|
|
||||||
let tool_call_id = if sender_type == AiContextSenderType::FunctionResult {
|
|
||||||
summary.tool_call_id
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
Self {
|
|
||||||
uid: summary.id,
|
|
||||||
sender_type,
|
|
||||||
sender_uid: summary.sender_id,
|
|
||||||
sender_name: Some(summary.sender_name),
|
|
||||||
content: summary.content,
|
|
||||||
content_type: summary.content_type,
|
|
||||||
send_at: summary.send_at,
|
|
||||||
tool_call_id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,508 +0,0 @@
|
|||||||
use models::projects::project_skill;
|
|
||||||
use sea_orm::*;
|
|
||||||
|
|
||||||
use super::context::RoomMessageContext;
|
|
||||||
use super::{AiRequest, Mention};
|
|
||||||
use crate::client::types::ChatRequestMessage;
|
|
||||||
use crate::compact::CompactService;
|
|
||||||
use crate::embed::EmbedService;
|
|
||||||
use crate::error::Result;
|
|
||||||
use crate::perception::{PerceptionService, SkillContext, SkillEntry};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct MessageBuilder {
|
|
||||||
pub compact_service: Option<CompactService>,
|
|
||||||
pub embed_service: Option<EmbedService>,
|
|
||||||
pub perception_service: PerceptionService,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MessageBuilder {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
compact_service: None,
|
|
||||||
embed_service: None,
|
|
||||||
perception_service: PerceptionService::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_compact_service(mut self, cs: CompactService) -> Self {
|
|
||||||
self.compact_service = Some(cs);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_embed_service(mut self, es: EmbedService) -> Self {
|
|
||||||
self.embed_service = Some(es);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_perception_service(mut self, ps: PerceptionService) -> Self {
|
|
||||||
self.perception_service = ps;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn build_messages(&self, request: &AiRequest) -> Result<Vec<ChatRequestMessage>> {
|
|
||||||
let mut messages = Vec::new();
|
|
||||||
|
|
||||||
if let Some(ref preamble) = request.room_preamble {
|
|
||||||
messages.push(ChatRequestMessage::system(preamble.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
messages.push(ChatRequestMessage::system(
|
|
||||||
"When receiving a question or problem, follow this reasoning process:\n\
|
|
||||||
1. ANALYZE: Break down the question. Identify what is being asked, what context is available, and what information is missing.\n\
|
|
||||||
2. GATHER: Use available tools (repository search, file reading, etc.) to collect relevant information before answering.\n\
|
|
||||||
3. REASON: Synthesize the gathered information. Consider edge cases and potential issues.\n\
|
|
||||||
4. ANSWER: Provide a clear, actionable answer based on your analysis.\n\
|
|
||||||
\n\
|
|
||||||
Do NOT guess or assume when tools can provide concrete answers. Always verify claims against actual code or documentation.".to_string()
|
|
||||||
));
|
|
||||||
if let Some(system_prompt) = request
|
|
||||||
.execution_profile
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|p| p.system_prompt.as_ref())
|
|
||||||
{
|
|
||||||
messages.push(ChatRequestMessage::system(system_prompt.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut processed_history = Vec::new();
|
|
||||||
if let Some(compact_service) = &self.compact_service {
|
|
||||||
let compact_config = request
|
|
||||||
.context_setting
|
|
||||||
.as_ref()
|
|
||||||
.map(|s| {
|
|
||||||
crate::compact::CompactConfig::from_project_setting(
|
|
||||||
s.context_window_tokens,
|
|
||||||
s.compaction_threshold,
|
|
||||||
s.compaction_max_summary_ratio,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
match compact_service
|
|
||||||
.for_model_entry(&request.model)
|
|
||||||
.prepare_room_compact_context(
|
|
||||||
request.room.id,
|
|
||||||
request.sender.uid,
|
|
||||||
Some(request.user_names.clone()),
|
|
||||||
compact_config,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(context) => {
|
|
||||||
if let Some(summary) = context.summary.filter(|s| !s.trim().is_empty()) {
|
|
||||||
messages.push(ChatRequestMessage::system(format!(
|
|
||||||
"Conversation summary:\n{}",
|
|
||||||
summary
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
processed_history = context.retained;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
tracing::warn!(error = %e, "conversation compaction failed, using full history");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !processed_history.is_empty() {
|
|
||||||
for msg_summary in processed_history {
|
|
||||||
let ctx = RoomMessageContext::from(msg_summary);
|
|
||||||
messages.push(ctx.to_message());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for msg in Self::history_in_chronological_order(&request.history) {
|
|
||||||
let ctx = RoomMessageContext::from_model_with_names(msg, &request.user_names);
|
|
||||||
messages.push(ctx.to_message());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for mention in &request.mention {
|
|
||||||
match mention {
|
|
||||||
Mention::Repo(repo) => {
|
|
||||||
let mut parts = vec![
|
|
||||||
format!("Name: {}", repo.repo_name),
|
|
||||||
format!("ID: {}", repo.id),
|
|
||||||
];
|
|
||||||
if let Some(ref desc) = repo.description {
|
|
||||||
parts.push(format!("Description: {}", desc));
|
|
||||||
}
|
|
||||||
parts.push(format!("Default branch: {}", repo.default_branch));
|
|
||||||
parts.push(format!(
|
|
||||||
"Private: {}",
|
|
||||||
if repo.is_private { "yes" } else { "no" }
|
|
||||||
));
|
|
||||||
parts.push(format!("Created: {}", repo.created_at.format("%Y-%m-%d")));
|
|
||||||
messages.push(ChatRequestMessage::system(format!(
|
|
||||||
"Mentioned repository:\n{}",
|
|
||||||
parts.join("\n")
|
|
||||||
)));
|
|
||||||
|
|
||||||
if let Some(embed_service) = &self.embed_service {
|
|
||||||
let query = format!(
|
|
||||||
"{} {}",
|
|
||||||
repo.repo_name,
|
|
||||||
repo.description.as_deref().unwrap_or_default()
|
|
||||||
);
|
|
||||||
if let Ok(issues) = embed_service.search_issues(&query, 5).await {
|
|
||||||
if !issues.is_empty() {
|
|
||||||
let context = format!(
|
|
||||||
"Related issues for repo {}:\n{}",
|
|
||||||
repo.repo_name,
|
|
||||||
issues
|
|
||||||
.iter()
|
|
||||||
.map(|i| format!("- {}", i.payload.text))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("\n")
|
|
||||||
);
|
|
||||||
messages.push(ChatRequestMessage::system(context));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Ok(repos) = embed_service.search_repos(&query, 3).await {
|
|
||||||
if !repos.is_empty() {
|
|
||||||
let context = format!(
|
|
||||||
"Similar repositories:\n{}",
|
|
||||||
repos
|
|
||||||
.iter()
|
|
||||||
.map(|r| format!("- {}", r.payload.text))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("\n")
|
|
||||||
);
|
|
||||||
messages.push(ChatRequestMessage::system(context));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Mention::User(user) => {
|
|
||||||
let mut profile_parts = vec![format!("Username: {}", user.username)];
|
|
||||||
if let Some(ref display_name) = user.display_name {
|
|
||||||
profile_parts.push(format!("Display name: {}", display_name));
|
|
||||||
}
|
|
||||||
if let Some(ref org) = user.organization {
|
|
||||||
profile_parts.push(format!("Organization: {}", org));
|
|
||||||
}
|
|
||||||
if let Some(ref website) = user.website_url {
|
|
||||||
profile_parts.push(format!("Website: {}", website));
|
|
||||||
}
|
|
||||||
messages.push(ChatRequestMessage::system(format!(
|
|
||||||
"Mentioned user profile:\n{}",
|
|
||||||
profile_parts.join("\n")
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for ctx in self.build_skill_context(request).await {
|
|
||||||
messages.push(ctx.to_system_message());
|
|
||||||
}
|
|
||||||
for mem in self.build_memory_context(request).await {
|
|
||||||
messages.push(mem.to_system_message());
|
|
||||||
}
|
|
||||||
|
|
||||||
messages.push(ChatRequestMessage::system(format!(
|
|
||||||
"Current Project:\n{}\nDescription: {}\nPublic: {}",
|
|
||||||
request.project.display_name,
|
|
||||||
request.project.description.as_deref().unwrap_or("(none)"),
|
|
||||||
if request.project.is_public {
|
|
||||||
"yes"
|
|
||||||
} else {
|
|
||||||
"no"
|
|
||||||
}
|
|
||||||
)));
|
|
||||||
|
|
||||||
let mut sender_parts = vec![format!("**Sender:** {}", request.sender.username)];
|
|
||||||
if let Some(ref display_name) = request.sender.display_name {
|
|
||||||
sender_parts.push(display_name.clone());
|
|
||||||
}
|
|
||||||
if let Some(ref org) = request.sender.organization {
|
|
||||||
sender_parts.push(format!("({})", org));
|
|
||||||
}
|
|
||||||
messages.push(ChatRequestMessage::system(format!(
|
|
||||||
"The person sending the next message:\n{}",
|
|
||||||
sender_parts.join(" ")
|
|
||||||
)));
|
|
||||||
messages.push(ChatRequestMessage::user(&request.input));
|
|
||||||
Ok(messages)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn history_in_chronological_order(
|
|
||||||
history: &[models::rooms::room_message::Model],
|
|
||||||
) -> Vec<&models::rooms::room_message::Model> {
|
|
||||||
let mut ordered = history.iter().collect::<Vec<_>>();
|
|
||||||
ordered.sort_by_key(|m| (m.seq, m.send_at));
|
|
||||||
ordered
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn build_room_optimized_context_text(
|
|
||||||
&self,
|
|
||||||
request: &AiRequest,
|
|
||||||
) -> Result<(String, Option<i64>)> {
|
|
||||||
let Some(compact_service) = &self.compact_service else {
|
|
||||||
return Ok((Self::recent_history_text(request, None), None));
|
|
||||||
};
|
|
||||||
|
|
||||||
let compact_config = request
|
|
||||||
.context_setting
|
|
||||||
.as_ref()
|
|
||||||
.map(|s| {
|
|
||||||
crate::compact::CompactConfig::from_project_setting(
|
|
||||||
s.context_window_tokens,
|
|
||||||
s.compaction_threshold,
|
|
||||||
s.compaction_max_summary_ratio,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let context = match compact_service
|
|
||||||
.for_model_entry(&request.model)
|
|
||||||
.prepare_room_compact_context(
|
|
||||||
request.room.id,
|
|
||||||
request.sender.uid,
|
|
||||||
Some(request.user_names.clone()),
|
|
||||||
compact_config,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(context) => context,
|
|
||||||
Err(e) => {
|
|
||||||
tracing::warn!(error = %e, "room compact context unavailable; using recent history");
|
|
||||||
return Ok((Self::recent_history_text(request, None), None));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut parts = Vec::new();
|
|
||||||
if let Some(summary) = context.summary.as_ref().filter(|s| !s.trim().is_empty()) {
|
|
||||||
parts.push(format!(
|
|
||||||
"### Compressed Conversation Before Seq {}\n{}",
|
|
||||||
context.cutoff_seq.unwrap_or_default(),
|
|
||||||
summary
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if !context.retained.is_empty() {
|
|
||||||
parts.push(format!(
|
|
||||||
"### Recent Conversation After Compression\n{}",
|
|
||||||
crate::compact::helpers::retained_as_text(&context.retained)
|
|
||||||
));
|
|
||||||
} else if parts.is_empty() {
|
|
||||||
parts.push(Self::recent_history_text(request, context.cutoff_seq));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok((parts.join("\n\n"), context.cutoff_seq))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn recent_history_text(request: &AiRequest, cutoff_seq: Option<i64>) -> String {
|
|
||||||
let mut lines = Vec::new();
|
|
||||||
for msg in Self::history_in_chronological_order(&request.history)
|
|
||||||
.into_iter()
|
|
||||||
.filter(|m| cutoff_seq.map(|seq| m.seq > seq).unwrap_or(true))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.into_iter()
|
|
||||||
.rev()
|
|
||||||
.take(20)
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.into_iter()
|
|
||||||
.rev()
|
|
||||||
{
|
|
||||||
let author = msg
|
|
||||||
.sender_id
|
|
||||||
.and_then(|uid| request.user_names.get(&uid))
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_else(|| msg.sender_type.to_string());
|
|
||||||
let content = msg.content.split_whitespace().collect::<Vec<_>>().join(" ");
|
|
||||||
let content = if content.len() > 500 {
|
|
||||||
format!("{}...", content.chars().take(500).collect::<String>())
|
|
||||||
} else {
|
|
||||||
content
|
|
||||||
};
|
|
||||||
lines.push(format!("[{}] {}: {}", msg.send_at, author, content));
|
|
||||||
}
|
|
||||||
if lines.is_empty() {
|
|
||||||
"### Recent Conversation\nnone".to_string()
|
|
||||||
} else {
|
|
||||||
format!("### Recent Conversation\n{}", lines.join("\n"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn build_skill_context(
|
|
||||||
&self,
|
|
||||||
request: &AiRequest,
|
|
||||||
) -> Vec<crate::perception::SkillContext> {
|
|
||||||
let db_skills: Vec<SkillEntry> = match project_skill::Entity::find()
|
|
||||||
.filter(project_skill::Column::ProjectUuid.eq(request.project.id))
|
|
||||||
.filter(project_skill::Column::Enabled.eq(true))
|
|
||||||
.all(&request.db)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(models) => models
|
|
||||||
.into_iter()
|
|
||||||
.map(|s| SkillEntry {
|
|
||||||
slug: s.slug,
|
|
||||||
name: s.name,
|
|
||||||
description: s.description,
|
|
||||||
content: s.content,
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
Err(_) => Vec::new(),
|
|
||||||
};
|
|
||||||
let mut all_skills: Vec<SkillEntry> = db_skills;
|
|
||||||
for built_in in crate::skills::all_skills() {
|
|
||||||
if !all_skills.iter().any(|s| s.slug == built_in.slug) {
|
|
||||||
all_skills.push(SkillEntry {
|
|
||||||
slug: built_in.slug.to_string(),
|
|
||||||
name: built_in.name.to_string(),
|
|
||||||
description: Some(built_in.description.to_string()),
|
|
||||||
content: built_in.content.clone(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if all_skills.is_empty() {
|
|
||||||
return Vec::new();
|
|
||||||
}
|
|
||||||
|
|
||||||
let history_texts: Vec<String> = request
|
|
||||||
.history
|
|
||||||
.iter()
|
|
||||||
.rev()
|
|
||||||
.take(10)
|
|
||||||
.map(|msg| msg.content.clone())
|
|
||||||
.collect();
|
|
||||||
let keyword_skills = self
|
|
||||||
.perception_service
|
|
||||||
.inject_skills(&request.input, &history_texts, &[], &all_skills)
|
|
||||||
.await;
|
|
||||||
let mut vector_skills = Vec::new();
|
|
||||||
if let Some(es) = &self.embed_service {
|
|
||||||
let rag_enabled = request
|
|
||||||
.context_setting
|
|
||||||
.as_ref()
|
|
||||||
.map(|s| s.rag_enabled)
|
|
||||||
.unwrap_or(true);
|
|
||||||
if rag_enabled {
|
|
||||||
let max_results = request
|
|
||||||
.context_setting
|
|
||||||
.as_ref()
|
|
||||||
.map(|s| s.rag_max_results as usize)
|
|
||||||
.unwrap_or(3);
|
|
||||||
let min_score = request
|
|
||||||
.context_setting
|
|
||||||
.as_ref()
|
|
||||||
.map(|s| s.rag_min_score)
|
|
||||||
.unwrap_or(0.70);
|
|
||||||
let awareness =
|
|
||||||
crate::perception::VectorActiveAwareness::new(max_results, min_score);
|
|
||||||
vector_skills = awareness
|
|
||||||
.detect(es, &request.input, &request.project.id.to_string())
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let mut result = Vec::new();
|
|
||||||
for ctx in keyword_skills {
|
|
||||||
Self::push_unique_skill_context(&mut result, ctx);
|
|
||||||
}
|
|
||||||
for ctx in vector_skills {
|
|
||||||
Self::push_unique_skill_context(&mut result, ctx);
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn push_unique_skill_context(result: &mut Vec<SkillContext>, ctx: SkillContext) {
|
|
||||||
let key = ctx.dedupe_key();
|
|
||||||
if result.iter().any(|existing| existing.dedupe_key() == key) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
result.push(ctx);
|
|
||||||
result.sort_by_key(|ctx| ctx.activation.rank());
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn build_memory_context(
|
|
||||||
&self,
|
|
||||||
request: &AiRequest,
|
|
||||||
) -> Vec<crate::perception::vector::MemoryContext> {
|
|
||||||
let rag_enabled = request
|
|
||||||
.context_setting
|
|
||||||
.as_ref()
|
|
||||||
.map(|s| s.rag_enabled)
|
|
||||||
.unwrap_or(true);
|
|
||||||
if !rag_enabled {
|
|
||||||
return Vec::new();
|
|
||||||
}
|
|
||||||
match &self.embed_service {
|
|
||||||
Some(es) => {
|
|
||||||
let max_results = request
|
|
||||||
.context_setting
|
|
||||||
.as_ref()
|
|
||||||
.map(|s| s.rag_max_results as usize)
|
|
||||||
.unwrap_or(3);
|
|
||||||
let min_score = request
|
|
||||||
.context_setting
|
|
||||||
.as_ref()
|
|
||||||
.map(|s| s.rag_min_score)
|
|
||||||
.unwrap_or(0.72);
|
|
||||||
let awareness =
|
|
||||||
crate::perception::VectorPassiveAwareness::new(max_results, min_score);
|
|
||||||
awareness
|
|
||||||
.detect(
|
|
||||||
es,
|
|
||||||
&request.input,
|
|
||||||
&request.project.display_name,
|
|
||||||
&request.room.id.to_string(),
|
|
||||||
request.history_cutoff_seq,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
None => Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::MessageBuilder;
|
|
||||||
use chrono::{Duration, Utc};
|
|
||||||
use models::rooms::{MessageContentType, MessageSenderType, room_message};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn history_is_sorted_chronologically() {
|
|
||||||
let now = Utc::now();
|
|
||||||
let later = room_message::Model {
|
|
||||||
id: Uuid::new_v4(),
|
|
||||||
seq: 20,
|
|
||||||
room: Uuid::new_v4(),
|
|
||||||
sender_type: MessageSenderType::User,
|
|
||||||
sender_id: Some(Uuid::new_v4()),
|
|
||||||
model_id: None,
|
|
||||||
thread: None,
|
|
||||||
content: "later".into(),
|
|
||||||
content_type: MessageContentType::Text,
|
|
||||||
thinking_content: None,
|
|
||||||
edited_at: None,
|
|
||||||
send_at: now,
|
|
||||||
revoked: None,
|
|
||||||
revoked_by: None,
|
|
||||||
in_reply_to: None,
|
|
||||||
content_tsv: None,
|
|
||||||
};
|
|
||||||
let earlier = room_message::Model {
|
|
||||||
id: Uuid::new_v4(),
|
|
||||||
seq: 10,
|
|
||||||
room: later.room,
|
|
||||||
sender_type: MessageSenderType::User,
|
|
||||||
sender_id: later.sender_id,
|
|
||||||
model_id: None,
|
|
||||||
thread: None,
|
|
||||||
content: "earlier".into(),
|
|
||||||
content_type: MessageContentType::Text,
|
|
||||||
thinking_content: None,
|
|
||||||
edited_at: None,
|
|
||||||
send_at: now - Duration::seconds(10),
|
|
||||||
revoked: None,
|
|
||||||
revoked_by: None,
|
|
||||||
in_reply_to: None,
|
|
||||||
content_tsv: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let history = [later, earlier];
|
|
||||||
let ordered = MessageBuilder::history_in_chronological_order(&history);
|
|
||||||
assert_eq!(ordered[0].content, "earlier");
|
|
||||||
assert_eq!(ordered[1].content, "later");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,292 +0,0 @@
|
|||||||
use models::projects::project_skill;
|
|
||||||
use models::rooms::room_ai;
|
|
||||||
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use super::AiRequest;
|
|
||||||
use super::message_builder::MessageBuilder;
|
|
||||||
use super::service::ProcessResult;
|
|
||||||
use super::session_recording::record_ai_session;
|
|
||||||
use crate::client::AiClientConfig;
|
|
||||||
use crate::client::types::ChatRequestMessage;
|
|
||||||
use crate::error::Result;
|
|
||||||
use crate::perception::{SkillEntry, ToolCallEvent};
|
|
||||||
use crate::tool::{ToolCall as AgentToolCall, ToolContext, ToolExecutor};
|
|
||||||
|
|
||||||
pub async fn execute_process(
|
|
||||||
request: AiRequest,
|
|
||||||
message_builder: &MessageBuilder,
|
|
||||||
tool_registry: &Option<crate::tool::registry::ToolRegistry>,
|
|
||||||
ai_base_url: Option<String>,
|
|
||||||
ai_api_key: Option<String>,
|
|
||||||
) -> Result<ProcessResult> {
|
|
||||||
let tools: Vec<serde_json::Value> = request.tools.clone().unwrap_or_default();
|
|
||||||
let tools_enabled = !tools.is_empty();
|
|
||||||
let max_tool_depth = request.max_tool_depth;
|
|
||||||
|
|
||||||
let mut messages = message_builder.build_messages(&request).await?;
|
|
||||||
|
|
||||||
let room_ai_config = room_ai::Entity::find()
|
|
||||||
.filter(room_ai::Column::Room.eq(request.room.id))
|
|
||||||
.filter(room_ai::Column::Model.eq(request.model.id))
|
|
||||||
.one(&request.db)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let model_name = request.model.name.clone();
|
|
||||||
let profile = request.execution_profile.as_ref();
|
|
||||||
let temperature = profile
|
|
||||||
.and_then(|p| p.temperature.map(|v| v as f32))
|
|
||||||
.or_else(|| {
|
|
||||||
room_ai_config
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|r| r.temperature.map(|v| v as f32))
|
|
||||||
})
|
|
||||||
.unwrap_or(request.temperature as f32);
|
|
||||||
let max_tokens = profile
|
|
||||||
.and_then(|p| p.max_tokens.map(|v| v as u32))
|
|
||||||
.or_else(|| {
|
|
||||||
room_ai_config
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|r| r.max_tokens.map(|v| v as u32))
|
|
||||||
})
|
|
||||||
.unwrap_or(request.max_tokens as u32);
|
|
||||||
let mut tool_depth = 0;
|
|
||||||
let mut input_tokens = 0i64;
|
|
||||||
let mut output_tokens = 0i64;
|
|
||||||
let session_id = Uuid::now_v7();
|
|
||||||
let session_start = std::time::Instant::now();
|
|
||||||
let version_id = room_ai_config.as_ref().and_then(|r| r.version);
|
|
||||||
|
|
||||||
let config = AiClientConfig::new(ai_api_key.unwrap_or_default())
|
|
||||||
.with_base_url(ai_base_url.unwrap_or_else(|| "https://api.openai.com".into()));
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let response = crate::client::call_with_params(
|
|
||||||
&messages,
|
|
||||||
&model_name,
|
|
||||||
&config,
|
|
||||||
temperature,
|
|
||||||
max_tokens,
|
|
||||||
None,
|
|
||||||
if tools_enabled { Some(&tools) } else { None },
|
|
||||||
if tools_enabled { None } else { Some("none") },
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let text = response.content.clone();
|
|
||||||
input_tokens += response.input_tokens;
|
|
||||||
output_tokens += response.output_tokens;
|
|
||||||
|
|
||||||
if tools_enabled && !response.tool_calls.is_empty() {
|
|
||||||
messages.push(ChatRequestMessage::assistant(
|
|
||||||
Some(text.clone()),
|
|
||||||
Some(response.tool_calls.clone()),
|
|
||||||
));
|
|
||||||
|
|
||||||
let calls: Vec<AgentToolCall> = response
|
|
||||||
.tool_calls
|
|
||||||
.iter()
|
|
||||||
.map(|tc| AgentToolCall {
|
|
||||||
id: tc.id.clone(),
|
|
||||||
name: tc.function.name.clone(),
|
|
||||||
arguments: tc.function.arguments.clone(),
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let tool_names: Vec<String> = calls.iter().map(|call| call.name.clone()).collect();
|
|
||||||
|
|
||||||
let tool_messages =
|
|
||||||
execute_tools(&request, &calls, session_id, tool_registry, message_builder).await;
|
|
||||||
messages.extend(tool_messages);
|
|
||||||
inject_passive_skills(&request, message_builder, &tool_names, &mut messages).await;
|
|
||||||
|
|
||||||
tool_depth += 1;
|
|
||||||
if tool_depth >= max_tool_depth {
|
|
||||||
let content = if text.is_empty() {
|
|
||||||
format!(
|
|
||||||
"[AI reached maximum tool depth ({}) — no final answer produced]",
|
|
||||||
max_tool_depth
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
text
|
|
||||||
};
|
|
||||||
record_ai_session(
|
|
||||||
&request.cache,
|
|
||||||
&request.db,
|
|
||||||
request.project.id,
|
|
||||||
request.sender.uid,
|
|
||||||
session_id,
|
|
||||||
request.room.id,
|
|
||||||
request.model.id,
|
|
||||||
version_id.unwrap_or_default(),
|
|
||||||
input_tokens,
|
|
||||||
output_tokens,
|
|
||||||
session_start.elapsed().as_millis() as i64,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
return Ok(ProcessResult {
|
|
||||||
content,
|
|
||||||
input_tokens,
|
|
||||||
output_tokens,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
record_ai_session(
|
|
||||||
&request.cache,
|
|
||||||
&request.db,
|
|
||||||
request.project.id,
|
|
||||||
request.sender.uid,
|
|
||||||
session_id,
|
|
||||||
request.room.id,
|
|
||||||
request.model.id,
|
|
||||||
version_id.unwrap_or_default(),
|
|
||||||
input_tokens,
|
|
||||||
output_tokens,
|
|
||||||
session_start.elapsed().as_millis() as i64,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
return Ok(ProcessResult {
|
|
||||||
content: text,
|
|
||||||
input_tokens,
|
|
||||||
output_tokens,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn execute_tools(
|
|
||||||
request: &AiRequest,
|
|
||||||
calls: &[AgentToolCall],
|
|
||||||
session_id: Uuid,
|
|
||||||
tool_registry: &Option<crate::tool::registry::ToolRegistry>,
|
|
||||||
message_builder: &MessageBuilder,
|
|
||||||
) -> Vec<ChatRequestMessage> {
|
|
||||||
let mut ctx = ToolContext::new(
|
|
||||||
request.db.clone(),
|
|
||||||
request.cache.clone(),
|
|
||||||
request.config.clone(),
|
|
||||||
request.room.id,
|
|
||||||
Some(request.sender.uid),
|
|
||||||
)
|
|
||||||
.with_project(request.project.id);
|
|
||||||
if let Some(ref es) = message_builder.embed_service {
|
|
||||||
ctx = ctx.with_embed_service(es.clone());
|
|
||||||
}
|
|
||||||
if let Some(registry) = tool_registry {
|
|
||||||
ctx.registry_mut().merge(registry.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
let recorder =
|
|
||||||
crate::tool::recorder::ToolCallRecorder::with_session(request.db.clone(), session_id);
|
|
||||||
let start = std::time::Instant::now();
|
|
||||||
let executor = ToolExecutor::new();
|
|
||||||
match executor.execute_batch(calls.to_vec(), &mut ctx).await {
|
|
||||||
Ok(results) => {
|
|
||||||
for (call, result) in calls.iter().zip(results.iter()) {
|
|
||||||
let elapsed = start.elapsed().as_millis() as i64;
|
|
||||||
let is_error = matches!(result.result, crate::tool::ToolResult::Error(_));
|
|
||||||
let error_msg = match &result.result {
|
|
||||||
crate::tool::ToolResult::Error(msg) => Some(msg.clone()),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
recorder.record(crate::tool::recorder::ToolCallRecord {
|
|
||||||
tool_call_id: Uuid::new_v4().to_string(),
|
|
||||||
session_id: recorder.session_id(),
|
|
||||||
tool_name: call.name.clone(),
|
|
||||||
caller: request.sender.uid,
|
|
||||||
arguments: call
|
|
||||||
.arguments_json()
|
|
||||||
.unwrap_or_else(|_| serde_json::Value::Null),
|
|
||||||
status: if is_error {
|
|
||||||
models::ai::ToolCallStatus::Failed
|
|
||||||
} else {
|
|
||||||
models::ai::ToolCallStatus::Success
|
|
||||||
},
|
|
||||||
execution_time_ms: Some(elapsed),
|
|
||||||
error_message: error_msg,
|
|
||||||
error_stack: None,
|
|
||||||
retry_count: 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
crate::tool::ToolExecutor::to_tool_messages(&results)
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
let elapsed = start.elapsed().as_millis() as i64;
|
|
||||||
for call in calls {
|
|
||||||
recorder.record(crate::tool::recorder::ToolCallRecord {
|
|
||||||
tool_call_id: Uuid::new_v4().to_string(),
|
|
||||||
session_id: recorder.session_id(),
|
|
||||||
tool_name: call.name.clone(),
|
|
||||||
caller: request.sender.uid,
|
|
||||||
arguments: call
|
|
||||||
.arguments_json()
|
|
||||||
.unwrap_or_else(|_| serde_json::Value::Null),
|
|
||||||
status: models::ai::ToolCallStatus::Failed,
|
|
||||||
execution_time_ms: Some(elapsed),
|
|
||||||
error_message: Some(e.to_string()),
|
|
||||||
error_stack: None,
|
|
||||||
retry_count: 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let err_msg = format!("[Tool call failed: {}]", e);
|
|
||||||
calls
|
|
||||||
.iter()
|
|
||||||
.map(|_| ChatRequestMessage::tool(Uuid::new_v4().to_string(), &err_msg))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn inject_passive_skills(
|
|
||||||
request: &AiRequest,
|
|
||||||
message_builder: &MessageBuilder,
|
|
||||||
tool_names: &[String],
|
|
||||||
messages: &mut Vec<ChatRequestMessage>,
|
|
||||||
) {
|
|
||||||
if let Ok(skills) = project_skill::Entity::find()
|
|
||||||
.filter(project_skill::Column::ProjectUuid.eq(request.project.id))
|
|
||||||
.filter(project_skill::Column::Enabled.eq(true))
|
|
||||||
.all(&request.db)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
let mut skill_entries: Vec<SkillEntry> = skills
|
|
||||||
.into_iter()
|
|
||||||
.map(|s| SkillEntry {
|
|
||||||
slug: s.slug,
|
|
||||||
name: s.name,
|
|
||||||
description: s.description,
|
|
||||||
content: s.content,
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
for built_in in crate::skills::all_skills() {
|
|
||||||
if !skill_entries.iter().any(|s| s.slug == built_in.slug) {
|
|
||||||
skill_entries.push(SkillEntry {
|
|
||||||
slug: built_in.slug.to_string(),
|
|
||||||
name: built_in.name.to_string(),
|
|
||||||
description: Some(built_in.description.to_string()),
|
|
||||||
content: built_in.content.clone(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let tool_events: Vec<ToolCallEvent> = tool_names
|
|
||||||
.iter()
|
|
||||||
.map(|name| ToolCallEvent {
|
|
||||||
tool_name: name.clone(),
|
|
||||||
arguments: String::new(),
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let mut contexts = Vec::new();
|
|
||||||
for event in &tool_events {
|
|
||||||
if let Some(ctx) = message_builder
|
|
||||||
.perception_service
|
|
||||||
.passive
|
|
||||||
.detect(event, &skill_entries)
|
|
||||||
{
|
|
||||||
MessageBuilder::push_unique_skill_context(&mut contexts, ctx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for ctx in contexts {
|
|
||||||
messages.push(ctx.to_system_message());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,152 +0,0 @@
|
|||||||
use db::cache::AppCache;
|
|
||||||
use db::database::AppDatabase;
|
|
||||||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
/// Record an AI session with cost calculation.
|
|
||||||
pub async fn record_ai_session(
|
|
||||||
cache: &AppCache,
|
|
||||||
db: &AppDatabase,
|
|
||||||
project_id: Uuid,
|
|
||||||
user_id: Uuid,
|
|
||||||
session_id: Uuid,
|
|
||||||
room_id: Uuid,
|
|
||||||
model_id: Uuid,
|
|
||||||
version_id: Uuid,
|
|
||||||
input_tokens: i64,
|
|
||||||
output_tokens: i64,
|
|
||||||
latency_ms: i64,
|
|
||||||
) {
|
|
||||||
metrics::histogram!("ai_call_latency_ms", "model" => model_id.to_string())
|
|
||||||
.record(latency_ms as f64);
|
|
||||||
|
|
||||||
let session = models::ai::ai_session::ActiveModel {
|
|
||||||
id: sea_orm::Set(session_id),
|
|
||||||
room: sea_orm::Set(room_id),
|
|
||||||
model: sea_orm::Set(model_id),
|
|
||||||
version: sea_orm::Set(version_id),
|
|
||||||
token_input: sea_orm::Set(input_tokens),
|
|
||||||
token_output: sea_orm::Set(output_tokens),
|
|
||||||
latency_ms: sea_orm::Set(Some(latency_ms)),
|
|
||||||
cost: sea_orm::Set(None),
|
|
||||||
currency: sea_orm::Set(None),
|
|
||||||
error_message: sea_orm::Set(None),
|
|
||||||
error_code: sea_orm::Set(None),
|
|
||||||
created_at: sea_orm::Set(chrono::Utc::now()),
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(e) = session.insert(db).await {
|
|
||||||
tracing::error!(error = %e, session_id = %session_id, "failed to insert ai session record");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let (cost, currency, error_msg) = match crate::billing::record_ai_usage(
|
|
||||||
db,
|
|
||||||
project_id,
|
|
||||||
user_id,
|
|
||||||
version_id,
|
|
||||||
input_tokens,
|
|
||||||
output_tokens,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(crate::billing::BillingResult::Success(record)) => {
|
|
||||||
(Some(record.cost), Some(record.currency), None)
|
|
||||||
}
|
|
||||||
Ok(crate::billing::BillingResult::InsufficientBalance { message }) => {
|
|
||||||
create_billing_error_system_message(cache, db, room_id, &message).await;
|
|
||||||
(None, None, Some(message))
|
|
||||||
}
|
|
||||||
Err(e) => (None, None, Some(e.to_string())),
|
|
||||||
};
|
|
||||||
|
|
||||||
use sea_orm::sea_query::Expr;
|
|
||||||
let _ = models::ai::ai_session::Entity::update_many()
|
|
||||||
.col_expr(models::ai::ai_session::Column::Cost, Expr::value(cost))
|
|
||||||
.col_expr(
|
|
||||||
models::ai::ai_session::Column::Currency,
|
|
||||||
Expr::value(currency),
|
|
||||||
)
|
|
||||||
.col_expr(
|
|
||||||
models::ai::ai_session::Column::ErrorMessage,
|
|
||||||
Expr::value(error_msg),
|
|
||||||
)
|
|
||||||
.filter(models::ai::ai_session::Column::Id.eq(session_id))
|
|
||||||
.exec(db)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a system message in the room for billing errors.
|
|
||||||
async fn create_billing_error_system_message(
|
|
||||||
cache: &AppCache,
|
|
||||||
db: &AppDatabase,
|
|
||||||
room_id: Uuid,
|
|
||||||
message: &str,
|
|
||||||
) {
|
|
||||||
use models::rooms::{MessageContentType, MessageSenderType, room_message};
|
|
||||||
use sea_orm::Set;
|
|
||||||
|
|
||||||
let seq_key = format!("seq:room:{}", room_id);
|
|
||||||
let seq = match cache.conn().await {
|
|
||||||
Ok(mut conn) => {
|
|
||||||
match redis::cmd("INCR")
|
|
||||||
.arg(&seq_key)
|
|
||||||
.query_async::<i64>(&mut conn)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(s) => s,
|
|
||||||
Err(e) => {
|
|
||||||
tracing::warn!(error = %e, "cache INCR failed for system message seq, falling back to DB");
|
|
||||||
let last_seq = match room_message::Entity::find()
|
|
||||||
.filter(room_message::Column::Room.eq(room_id))
|
|
||||||
.order_by_desc(room_message::Column::Seq)
|
|
||||||
.one(db)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(Some(m)) => m.seq,
|
|
||||||
Ok(None) => 0,
|
|
||||||
Err(e) => {
|
|
||||||
tracing::warn!(error = %e, "Failed to get last seq for system message");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
last_seq + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
tracing::warn!(error = %e, "Failed to get Redis connection for system message seq");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let now = chrono::Utc::now();
|
|
||||||
let result = room_message::ActiveModel {
|
|
||||||
id: Set(Uuid::now_v7()),
|
|
||||||
seq: Set(seq),
|
|
||||||
room: Set(room_id),
|
|
||||||
sender_type: Set(MessageSenderType::System),
|
|
||||||
sender_id: Set(None),
|
|
||||||
model_id: Set(None),
|
|
||||||
thread: Set(None),
|
|
||||||
in_reply_to: Set(None),
|
|
||||||
content: Set(message.to_string()),
|
|
||||||
content_type: Set(MessageContentType::Text),
|
|
||||||
thinking_content: Set(None),
|
|
||||||
edited_at: Set(None),
|
|
||||||
send_at: Set(now),
|
|
||||||
revoked: Set(None),
|
|
||||||
revoked_by: Set(None),
|
|
||||||
}
|
|
||||||
.insert(db)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(_) => {
|
|
||||||
tracing::info!(room_id = %room_id, message = %message, "system_message_created_for_billing_error")
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
tracing::warn!(error = %e, room_id = %room_id, "Failed to create system message for billing error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user