opendev_repl/
query_processor.rs1use 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#[derive(Debug, Clone)]
21pub struct QueryResult {
22 pub content: String,
24 pub operation_summary: String,
26 pub error: Option<String>,
28 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
43pub struct QueryProcessor {
51 execution_count: u64,
53 working_dir: PathBuf,
55}
56
57impl QueryProcessor {
58 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 pub fn with_working_dir(working_dir: PathBuf) -> Self {
68 Self {
69 execution_count: 0,
70 working_dir,
71 }
72 }
73
74 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 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 let enhanced = self.enhance_query(query);
107
108 let mut messages = self.build_messages(&enhanced, session_manager);
110
111 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 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 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 format!("{}\n\n{}", query, result.text_content)
172 }
173
174 fn build_messages(&self, _query: &str, session_manager: &SessionManager) -> Vec<Value> {
176 let mut messages = Vec::new();
177
178 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;