Extend delegation system with 5 new specialized roles alongside researcher/analyst/reviewer. Each role has curated tool access. Refactor profile lookup to use profile_for_role_name and update compact/summarizer and tool context accordingly.
269 lines
8.5 KiB
Rust
269 lines
8.5 KiB
Rust
use super::message_builder::MessageBuilder;
|
|
use super::{AiRequest, StreamCallback};
|
|
use crate::client::AiClientConfig;
|
|
use crate::client::StreamChunk;
|
|
use crate::compact::CompactService;
|
|
use crate::embed::EmbedService;
|
|
use crate::error::Result;
|
|
use crate::perception::PerceptionService;
|
|
use crate::tool::registry::ToolRegistry;
|
|
use queue::MessageProducer;
|
|
|
|
/// Result from streaming AI response.
|
|
pub struct StreamResult {
|
|
pub content: String,
|
|
pub reasoning_content: String,
|
|
pub input_tokens: i64,
|
|
pub output_tokens: i64,
|
|
/// All chunks in arrival order — preserves ReAct multi-cycle ordering.
|
|
pub chunks: Vec<StreamChunk>,
|
|
}
|
|
|
|
/// Result from non-streaming AI response.
|
|
pub struct ProcessResult {
|
|
pub content: String,
|
|
pub input_tokens: i64,
|
|
pub output_tokens: i64,
|
|
}
|
|
|
|
/// Service for handling AI chat requests in rooms.
|
|
pub struct ChatService {
|
|
ai_base_url: Option<String>,
|
|
ai_api_key: Option<String>,
|
|
message_builder: MessageBuilder,
|
|
tool_registry: Option<ToolRegistry>,
|
|
}
|
|
|
|
impl ChatService {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
ai_base_url: None,
|
|
ai_api_key: None,
|
|
message_builder: MessageBuilder::new(),
|
|
tool_registry: None,
|
|
}
|
|
}
|
|
|
|
pub fn with_ai_client_config(mut self, config: AiClientConfig) -> Self {
|
|
self.ai_base_url = config.base_url.clone();
|
|
self.ai_api_key = Some(config.api_key.clone());
|
|
self
|
|
}
|
|
|
|
pub fn with_compact_service(mut self, compact_service: CompactService) -> Self {
|
|
self.message_builder = self.message_builder.with_compact_service(compact_service);
|
|
self
|
|
}
|
|
|
|
pub fn with_embed_service(mut self, embed_service: EmbedService) -> Self {
|
|
self.message_builder = self.message_builder.with_embed_service(embed_service);
|
|
self
|
|
}
|
|
|
|
pub fn with_perception_service(mut self, perception_service: PerceptionService) -> Self {
|
|
self.message_builder = self
|
|
.message_builder
|
|
.with_perception_service(perception_service);
|
|
self
|
|
}
|
|
|
|
pub fn with_tool_registry(mut self, registry: ToolRegistry) -> Self {
|
|
self.tool_registry = Some(registry);
|
|
self
|
|
}
|
|
|
|
/// Returns all registered tools as JSON tool definitions.
|
|
pub fn tools(&self) -> Vec<serde_json::Value> {
|
|
self.tool_registry
|
|
.as_ref()
|
|
.map(|r| r.to_openai_tools())
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
/// Build a RigToolSet from the registered tool registry.
|
|
///
|
|
/// This enables using the same tools with `RigAgentService` via rig's native Agent.
|
|
/// The context (db, cache, config, room_id, sender_id) is passed through to each
|
|
/// tool handler at creation time.
|
|
#[cfg(feature = "rig")]
|
|
pub fn rig_toolset(
|
|
&self,
|
|
db: db::database::AppDatabase,
|
|
cache: db::cache::AppCache,
|
|
config: config::AppConfig,
|
|
room_id: uuid::Uuid,
|
|
sender_id: Option<uuid::Uuid>,
|
|
project_id: uuid::Uuid,
|
|
) -> Option<crate::RigToolSet> {
|
|
self.tool_registry.as_ref().map(|registry| {
|
|
crate::RigToolSet::from_registry(
|
|
registry,
|
|
db,
|
|
cache,
|
|
config,
|
|
room_id,
|
|
sender_id,
|
|
project_id,
|
|
None,
|
|
None,
|
|
None,
|
|
std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
|
|
)
|
|
})
|
|
}
|
|
|
|
/// Get a reference to the underlying ToolRegistry.
|
|
pub fn tool_registry(&self) -> Option<&ToolRegistry> {
|
|
self.tool_registry.as_ref()
|
|
}
|
|
|
|
pub async fn build_room_optimized_context_text(
|
|
&self,
|
|
request: &AiRequest,
|
|
) -> Result<(String, Option<i64>)> {
|
|
self.message_builder
|
|
.build_room_optimized_context_text(request)
|
|
.await
|
|
}
|
|
|
|
/// Process AI request without streaming (tool-call loop with non-streaming API).
|
|
pub async fn process(&self, request: AiRequest) -> Result<ProcessResult> {
|
|
super::orchestrator::execute_orchestrated_process(
|
|
request,
|
|
&self.message_builder,
|
|
&self.tool_registry,
|
|
self.ai_base_url.clone(),
|
|
self.ai_api_key.clone(),
|
|
)
|
|
.await
|
|
}
|
|
|
|
/// Process AI request with streaming (tool-call loop with streaming API, incremental chunks).
|
|
pub async fn process_stream(
|
|
&self,
|
|
request: AiRequest,
|
|
on_chunk: StreamCallback,
|
|
) -> Result<StreamResult> {
|
|
super::orchestrator::execute_orchestrated_stream(
|
|
request,
|
|
on_chunk,
|
|
&self.message_builder,
|
|
&self.tool_registry,
|
|
self.ai_base_url.clone(),
|
|
self.ai_api_key.clone(),
|
|
)
|
|
.await
|
|
}
|
|
|
|
/// Process AI request for room context — direct execution path (bypasses orchestrator).
|
|
///
|
|
/// Room AI uses a fast single-agent loop: all tools available, no multi-agent delegation.
|
|
/// Merges `room_tools` (send_message, retract_message) into the base registry,
|
|
/// then runs `execute_process` / `execute_process_stream` directly.
|
|
pub async fn process_room(
|
|
&self,
|
|
request: AiRequest,
|
|
room_tools: ToolRegistry,
|
|
) -> Result<ProcessResult> {
|
|
let mut merged = self.tool_registry.clone().unwrap_or_default();
|
|
merged.merge(room_tools);
|
|
|
|
super::nonstreaming_execution::execute_process(
|
|
request,
|
|
&self.message_builder,
|
|
&Some(merged),
|
|
self.ai_base_url.clone(),
|
|
self.ai_api_key.clone(),
|
|
)
|
|
.await
|
|
}
|
|
|
|
/// Process AI request for room context with streaming — direct execution path.
|
|
///
|
|
/// Same as `process_room` but with streaming response. Bypasses orchestrator,
|
|
/// gives the room AI all tools (base + room) for fast single-agent execution.
|
|
pub async fn process_room_stream(
|
|
&self,
|
|
request: AiRequest,
|
|
on_chunk: StreamCallback,
|
|
room_tools: ToolRegistry,
|
|
) -> Result<StreamResult> {
|
|
let mut merged = self.tool_registry.clone().unwrap_or_default();
|
|
merged.merge(room_tools);
|
|
|
|
super::streaming_execution::execute_process_stream(
|
|
request,
|
|
on_chunk,
|
|
&self.message_builder,
|
|
&Some(merged),
|
|
self.ai_base_url.clone(),
|
|
self.ai_api_key.clone(),
|
|
)
|
|
.await
|
|
}
|
|
|
|
/// Process AI request via rig-based ReAct streaming loop.
|
|
pub async fn process_react<C, Fut>(
|
|
&self,
|
|
request: &AiRequest,
|
|
on_chunk: C,
|
|
) -> Result<(String, i64, i64)>
|
|
where
|
|
C: FnMut(crate::react::ReactStep) -> Fut + Send,
|
|
Fut: std::future::Future<Output = ()> + Send,
|
|
{
|
|
let Some(registry) = &self.tool_registry else {
|
|
return Err(crate::error::AgentError::Internal(
|
|
"no tool registry registered".into(),
|
|
));
|
|
};
|
|
super::react_execution::execute_process_react(
|
|
request,
|
|
on_chunk,
|
|
registry,
|
|
self.ai_base_url.clone(),
|
|
self.ai_api_key.clone(),
|
|
None,
|
|
None,
|
|
)
|
|
.await
|
|
}
|
|
|
|
/// Process AI request via rig-based ReAct streaming loop with room-specific tools.
|
|
///
|
|
/// Merges `room_tools` (e.g. `send_message`, `retract_message`) into the base
|
|
/// tool registry on-the-fly. The `room_preamble` is prepended to the default
|
|
/// system prompt to instruct the AI about room communication rules.
|
|
/// `message_producer` enables tools to publish events via the message queue.
|
|
pub async fn process_react_room<C, Fut>(
|
|
&self,
|
|
request: &AiRequest,
|
|
on_chunk: C,
|
|
room_tools: ToolRegistry,
|
|
room_preamble: Option<&str>,
|
|
message_producer: Option<MessageProducer>,
|
|
) -> Result<(String, i64, i64)>
|
|
where
|
|
C: FnMut(crate::react::ReactStep) -> Fut + Send,
|
|
Fut: std::future::Future<Output = ()> + Send,
|
|
{
|
|
let Some(registry) = &self.tool_registry else {
|
|
return Err(crate::error::AgentError::Internal(
|
|
"no tool registry registered".into(),
|
|
));
|
|
};
|
|
let mut merged = registry.clone();
|
|
merged.merge(room_tools);
|
|
super::react_execution::execute_process_react(
|
|
request,
|
|
on_chunk,
|
|
&merged,
|
|
self.ai_base_url.clone(),
|
|
self.ai_api_key.clone(),
|
|
room_preamble,
|
|
message_producer,
|
|
)
|
|
.await
|
|
}
|
|
}
|