Skip to main content

opendev_repl/
query_processor.rs

1//! Process user queries, enhance with context, delegate to ReAct loop.
2//!
3//! Mirrors `opendev/repl/query_processor.py`.
4
5use chrono::Utc;
6use serde_json::Value;
7use std::collections::HashMap;
8use std::path::PathBuf;
9use tracing::{debug, info};
10
11use crate::file_injector::FileContentInjector;
12
13use opendev_history::SessionManager;
14use opendev_models::{ChatMessage, Role};
15use opendev_tools_core::ToolRegistry;
16
17use crate::error::ReplError;
18
19/// Result of processing a query.
20#[derive(Debug, Clone)]
21pub struct QueryResult {
22    /// The assistant's response content.
23    pub content: String,
24    /// Summary of the last operation (for status display).
25    pub operation_summary: String,
26    /// Error message if something went wrong.
27    pub error: Option<String>,
28    /// LLM call latency in milliseconds.
29    pub latency_ms: Option<u64>,
30}
31
32impl Default for QueryResult {
33    fn default() -> Self {
34        Self {
35            content: String::new(),
36            operation_summary: String::from("—"),
37            error: None,
38            latency_ms: None,
39        }
40    }
41}
42
43/// Processes user queries using the ReAct pattern.
44///
45/// Coordinates:
46/// - Query enhancement (@ file references)
47/// - Message preparation
48/// - LLM calls with progress display
49/// - Tool execution
50pub struct QueryProcessor {
51    /// Number of queries processed in this session.
52    execution_count: u64,
53    /// Working directory for resolving @ file references.
54    working_dir: PathBuf,
55}
56
57impl QueryProcessor {
58    /// Create a new query processor.
59    pub fn new() -> Self {
60        Self {
61            execution_count: 0,
62            working_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
63        }
64    }
65
66    /// Create a new query processor with an explicit working directory.
67    pub fn with_working_dir(working_dir: PathBuf) -> Self {
68        Self {
69            execution_count: 0,
70            working_dir,
71        }
72    }
73
74    /// Process a user query.
75    ///
76    /// Adds the user message to the session, enhances the query with file
77    /// references, and delegates to the ReAct loop for execution.
78    pub async fn process(
79        &mut self,
80        query: &str,
81        session_manager: &mut SessionManager,
82        tool_registry: &ToolRegistry,
83        plan_requested: bool,
84    ) -> Result<QueryResult, ReplError> {
85        self.execution_count += 1;
86        info!(query_num = self.execution_count, "Processing query");
87
88        // Add user message to session
89        let user_msg = ChatMessage {
90            role: Role::User,
91            content: query.to_string(),
92            timestamp: Utc::now(),
93            metadata: HashMap::new(),
94            tool_calls: vec![],
95            tokens: None,
96            thinking_trace: None,
97            reasoning_content: None,
98            token_usage: None,
99            provenance: None,
100        };
101        if let Some(session) = session_manager.current_session_mut() {
102            session.messages.push(user_msg);
103        }
104
105        // Enhance query with @ file references
106        let enhanced = self.enhance_query(query);
107
108        // Build messages for the LLM
109        let mut messages = self.build_messages(&enhanced, session_manager);
110
111        // Inject plan reminder if plan mode is active
112        if plan_requested {
113            let plans_dir = dirs::home_dir()
114                .map(|h| h.join(".opendev").join("plans"))
115                .unwrap_or_else(|| std::path::PathBuf::from("/tmp"));
116            let plan_name = opendev_runtime::generate_plan_name(Some(&plans_dir), 50);
117            let plan_path = format!("~/.opendev/plans/{}.md", plan_name);
118            let reminder = opendev_agents::prompts::reminders::get_reminder(
119                "plan_subagent_request",
120                &[("plan_file_path", &plan_path)],
121            );
122            if !reminder.is_empty() {
123                messages.push(serde_json::json!({
124                    "role": "user",
125                    "content": format!("<system-reminder>{}</system-reminder>", reminder)
126                }));
127            }
128        }
129
130        // ReactLoop integration is deferred to the integration wiring phase (Step 9).
131        // The messages are prepared above; a caller holding an AgentRuntime will
132        // invoke ReactLoop::run() with these messages.
133        debug!(
134            tool_count = tool_registry.len(),
135            msg_count = messages.len(),
136            plan_requested,
137            "Ready for ReAct execution"
138        );
139
140        let mode_label = if plan_requested { "plan" } else { "normal" };
141        let result = QueryResult {
142            content: format!(
143                "[{} mode] Received query #{} ({} message(s) in context, {} tool(s) available):\n{}",
144                mode_label,
145                self.execution_count,
146                messages.len(),
147                tool_registry.len(),
148                enhanced,
149            ),
150            operation_summary: format!("Query #{}", self.execution_count),
151            error: None,
152            latency_ms: None,
153        };
154
155        Ok(result)
156    }
157
158    /// Enhance a query by resolving @ file references.
159    ///
160    /// Looks for patterns like `@path/to/file` and injects the file contents
161    /// into the query text.
162    fn enhance_query(&self, query: &str) -> String {
163        let injector = FileContentInjector::new(self.working_dir.clone());
164        let result = injector.inject_content(query);
165
166        if result.text_content.is_empty() {
167            return query.to_string();
168        }
169
170        // Append the injected file content after the original query.
171        format!("{}\n\n{}", query, result.text_content)
172    }
173
174    /// Build the message list for the LLM API call.
175    fn build_messages(&self, _query: &str, session_manager: &SessionManager) -> Vec<Value> {
176        let mut messages = Vec::new();
177
178        // Add conversation history from session
179        if let Some(session) = session_manager.current_session() {
180            for msg in &session.messages {
181                messages.push(serde_json::json!({
182                    "role": msg.role.to_string(),
183                    "content": &msg.content,
184                }));
185            }
186        }
187
188        messages
189    }
190}
191
192impl Default for QueryProcessor {
193    fn default() -> Self {
194        Self::new()
195    }
196}
197
198#[cfg(test)]
199#[path = "query_processor_tests.rs"]
200mod tests;