reflex/semantic/
schema_agentic.rs

1//! Agentic schema definitions for multi-step reasoning and context gathering
2//!
3//! This module defines the schema for agentic `rfx ask` which allows the LLM to:
4//! 1. Assess if it needs more context
5//! 2. Gather context using tools (rfx context, exploratory queries)
6//! 3. Generate final optimized queries
7//! 4. Evaluate results and refine if needed
8
9use serde::{Deserialize, Serialize};
10
11/// Agentic response from LLM with tool calling support
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct AgenticResponse {
14    /// Current phase of the agentic loop
15    pub phase: Phase,
16
17    /// LLM's reasoning/thought process
18    pub reasoning: String,
19
20    /// Whether more context is needed before generating final queries
21    #[serde(default)]
22    pub needs_context: bool,
23
24    /// Tool calls to gather additional context (only for assessment/gathering phases)
25    #[serde(default)]
26    pub tool_calls: Vec<ToolCall>,
27
28    /// Final query commands (only for final phase)
29    #[serde(default)]
30    pub queries: Vec<super::schema::QueryCommand>,
31
32    /// Confidence score (0.0-1.0) in the generated queries
33    #[serde(default)]
34    pub confidence: f32,
35}
36
37/// Phase of the agentic loop
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
39#[serde(rename_all = "lowercase")]
40pub enum Phase {
41    /// Initial assessment: determine if more context is needed
42    Assessment,
43
44    /// Context gathering: execute tool calls to collect information
45    Gathering,
46
47    /// Final query generation: produce the search queries
48    Final,
49
50    /// Evaluation: assess if results match user intent (internal phase, not from LLM)
51    #[serde(skip)]
52    Evaluation,
53
54    /// Refinement: regenerate queries based on evaluation (internal phase, not from LLM)
55    #[serde(skip)]
56    Refinement,
57}
58
59/// Tool call for gathering context or exploring codebase
60#[derive(Debug, Clone, Serialize, Deserialize)]
61#[serde(tag = "type", rename_all = "snake_case")]
62pub enum ToolCall {
63    /// Gather comprehensive codebase context
64    GatherContext {
65        /// Context gathering parameters
66        #[serde(flatten)]
67        params: ContextGatheringParams,
68    },
69
70    /// Run exploratory queries to understand codebase
71    ExploreCodebase {
72        /// Description of what this query is exploring
73        description: String,
74
75        /// The rfx query command (without 'rfx' prefix)
76        command: String,
77    },
78
79    /// Analyze codebase structure (hotspots, unused files, etc.)
80    AnalyzeStructure {
81        /// Type of analysis to run
82        analysis_type: AnalysisType,
83    },
84
85    /// Search project documentation files
86    SearchDocumentation {
87        /// Search query/keywords
88        query: String,
89
90        /// Optional: specific files to search (defaults to ["CLAUDE.md", "README.md"])
91        #[serde(default)]
92        files: Option<Vec<String>>,
93    },
94
95    /// Get index statistics (file counts, languages, etc.)
96    GetStatistics,
97
98    /// Get dependencies of a specific file
99    GetDependencies {
100        /// File path (supports fuzzy matching)
101        file_path: String,
102
103        /// Show reverse dependencies (what depends on this file)
104        #[serde(default)]
105        reverse: bool,
106    },
107
108    /// Get dependency analysis summary
109    GetAnalysisSummary {
110        /// Minimum dependents for hotspot counting
111        #[serde(default = "default_min_dependents")]
112        min_dependents: usize,
113    },
114
115    /// Find disconnected components (islands) in the dependency graph
116    FindIslands {
117        /// Minimum island size to include
118        #[serde(default = "default_min_island_size")]
119        min_size: usize,
120
121        /// Maximum island size to include
122        #[serde(default = "default_max_island_size")]
123        max_size: usize,
124    },
125}
126
127/// Parameters for context gathering tool
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct ContextGatheringParams {
130    /// Show directory structure
131    #[serde(default)]
132    pub structure: bool,
133
134    /// Show file type distribution
135    #[serde(default)]
136    pub file_types: bool,
137
138    /// Detect project type
139    #[serde(default)]
140    pub project_type: bool,
141
142    /// Detect frameworks
143    #[serde(default)]
144    pub framework: bool,
145
146    /// Show entry points
147    #[serde(default)]
148    pub entry_points: bool,
149
150    /// Show test layout
151    #[serde(default)]
152    pub test_layout: bool,
153
154    /// List configuration files
155    #[serde(default)]
156    pub config_files: bool,
157
158    /// Tree depth for structure (default: 2)
159    #[serde(default = "default_depth")]
160    pub depth: usize,
161
162    /// Focus on specific directory path
163    #[serde(default)]
164    pub path: Option<String>,
165}
166
167fn default_depth() -> usize {
168    2
169}
170
171fn default_min_dependents() -> usize {
172    2
173}
174
175fn default_min_island_size() -> usize {
176    2
177}
178
179fn default_max_island_size() -> usize {
180    500
181}
182
183impl Default for ContextGatheringParams {
184    fn default() -> Self {
185        Self {
186            structure: false,
187            file_types: false,
188            project_type: false,
189            framework: false,
190            entry_points: false,
191            test_layout: false,
192            config_files: false,
193            depth: default_depth(),
194            path: None,
195        }
196    }
197}
198
199/// Type of codebase analysis
200#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
201#[serde(rename_all = "lowercase")]
202pub enum AnalysisType {
203    /// Find most-imported files (dependency hotspots)
204    Hotspots,
205
206    /// Find unused files
207    Unused,
208
209    /// Find circular dependencies
210    Circular,
211}
212
213/// Result evaluation report
214#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct EvaluationReport {
216    /// Overall success assessment
217    pub success: bool,
218
219    /// Specific issues found with the results
220    pub issues: Vec<EvaluationIssue>,
221
222    /// Suggestions for refinement
223    pub suggestions: Vec<String>,
224
225    /// Evaluation score (0.0-1.0)
226    pub score: f32,
227}
228
229/// Specific issue found during result evaluation
230#[derive(Debug, Clone, Serialize, Deserialize)]
231pub struct EvaluationIssue {
232    /// Type of issue
233    pub issue_type: IssueType,
234
235    /// Description of the issue
236    pub description: String,
237
238    /// Severity (0.0-1.0, higher is more severe)
239    pub severity: f32,
240}
241
242/// Type of evaluation issue
243#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
244#[serde(rename_all = "snake_case")]
245pub enum IssueType {
246    /// No results found (query too specific or wrong pattern)
247    EmptyResults,
248
249    /// Too many results (query too broad)
250    TooManyResults,
251
252    /// Results in unexpected file types
253    WrongFileTypes,
254
255    /// Results in unexpected directories
256    WrongLocations,
257
258    /// Pattern doesn't match expected symbol type
259    WrongSymbolType,
260
261    /// Language filter seems incorrect
262    WrongLanguage,
263}
264
265/// JSON schema for agentic LLM prompt
266pub const AGENTIC_RESPONSE_SCHEMA: &str = r#"{
267  "type": "object",
268  "properties": {
269    "phase": {
270      "type": "string",
271      "enum": ["assessment", "gathering", "final"],
272      "description": "Current phase: 'assessment' if deciding whether to gather context, 'gathering' if executing tools, 'final' if generating queries"
273    },
274    "reasoning": {
275      "type": "string",
276      "description": "Your thought process and reasoning for this response"
277    },
278    "needs_context": {
279      "type": "boolean",
280      "description": "Whether you need more context before generating final queries (only relevant in assessment phase)"
281    },
282    "tool_calls": {
283      "type": "array",
284      "description": "Array of tools to execute for gathering context (only for assessment/gathering phases)",
285      "items": {
286        "type": "object",
287        "oneOf": [
288          {
289            "properties": {
290              "type": { "const": "gather_context" },
291              "structure": { "type": "boolean" },
292              "file_types": { "type": "boolean" },
293              "project_type": { "type": "boolean" },
294              "framework": { "type": "boolean" },
295              "entry_points": { "type": "boolean" },
296              "test_layout": { "type": "boolean" },
297              "config_files": { "type": "boolean" },
298              "depth": { "type": "integer" },
299              "path": { "type": "string" }
300            },
301            "required": ["type"]
302          },
303          {
304            "properties": {
305              "type": { "const": "explore_codebase" },
306              "description": { "type": "string" },
307              "command": { "type": "string" }
308            },
309            "required": ["type", "description", "command"]
310          },
311          {
312            "properties": {
313              "type": { "const": "analyze_structure" },
314              "analysis_type": { "type": "string", "enum": ["hotspots", "unused", "circular"] }
315            },
316            "required": ["type", "analysis_type"]
317          },
318          {
319            "properties": {
320              "type": { "const": "search_documentation" },
321              "query": { "type": "string" },
322              "files": {
323                "type": "array",
324                "items": { "type": "string" },
325                "description": "Optional: specific files to search (defaults to [\"CLAUDE.md\", \"README.md\"])"
326              }
327            },
328            "required": ["type", "query"]
329          },
330          {
331            "properties": {
332              "type": { "const": "get_statistics" }
333            },
334            "required": ["type"]
335          },
336          {
337            "properties": {
338              "type": { "const": "get_dependencies" },
339              "file_path": { "type": "string" },
340              "reverse": { "type": "boolean" }
341            },
342            "required": ["type", "file_path"]
343          },
344          {
345            "properties": {
346              "type": { "const": "get_analysis_summary" },
347              "min_dependents": { "type": "integer" }
348            },
349            "required": ["type"]
350          },
351          {
352            "properties": {
353              "type": { "const": "find_islands" },
354              "min_size": { "type": "integer" },
355              "max_size": { "type": "integer" }
356            },
357            "required": ["type"]
358          }
359        ]
360      }
361    },
362    "queries": {
363      "type": "array",
364      "description": "Array of rfx commands to execute (only for final phase)",
365      "items": {
366        "type": "object",
367        "properties": {
368          "command": { "type": "string" },
369          "order": { "type": "integer" },
370          "merge": { "type": "boolean" }
371        },
372        "required": ["command", "order", "merge"]
373      }
374    },
375    "confidence": {
376      "type": "number",
377      "minimum": 0.0,
378      "maximum": 1.0,
379      "description": "Confidence score (0.0-1.0) in your generated queries"
380    }
381  },
382  "required": ["phase", "reasoning"]
383}"#;
384
385#[cfg(test)]
386mod tests {
387    use super::*;
388
389    #[test]
390    fn test_deserialize_assessment_phase() {
391        let json = r#"{
392            "phase": "assessment",
393            "reasoning": "I need to understand the project structure",
394            "needs_context": true,
395            "tool_calls": [{
396                "type": "gather_context",
397                "structure": true,
398                "file_types": true
399            }],
400            "confidence": 0.0
401        }"#;
402
403        let response: AgenticResponse = serde_json::from_str(json).unwrap();
404        assert_eq!(response.phase, Phase::Assessment);
405        assert!(response.needs_context);
406        assert_eq!(response.tool_calls.len(), 1);
407    }
408
409    #[test]
410    fn test_deserialize_final_phase() {
411        let json = r#"{
412            "phase": "final",
413            "reasoning": "Based on the context, I can generate queries",
414            "needs_context": false,
415            "queries": [{
416                "command": "query \"TODO\"",
417                "order": 1,
418                "merge": true
419            }],
420            "confidence": 0.85
421        }"#;
422
423        let response: AgenticResponse = serde_json::from_str(json).unwrap();
424        assert_eq!(response.phase, Phase::Final);
425        assert!(!response.needs_context);
426        assert_eq!(response.queries.len(), 1);
427        assert_eq!(response.confidence, 0.85);
428    }
429
430    #[test]
431    fn test_deserialize_explore_tool() {
432        let json = r#"{
433            "type": "explore_codebase",
434            "description": "Find validation functions",
435            "command": "query \"validate\" --symbols --kind function"
436        }"#;
437
438        let tool: ToolCall = serde_json::from_str(json).unwrap();
439        match tool {
440            ToolCall::ExploreCodebase { description, command } => {
441                assert_eq!(description, "Find validation functions");
442                assert!(command.contains("validate"));
443            }
444            _ => panic!("Expected ExploreCodebase variant"),
445        }
446    }
447
448    #[test]
449    fn test_deserialize_analyze_tool() {
450        let json = r#"{
451            "type": "analyze_structure",
452            "analysis_type": "hotspots"
453        }"#;
454
455        let tool: ToolCall = serde_json::from_str(json).unwrap();
456        match tool {
457            ToolCall::AnalyzeStructure { analysis_type } => {
458                assert_eq!(analysis_type, AnalysisType::Hotspots);
459            }
460            _ => panic!("Expected AnalyzeStructure variant"),
461        }
462    }
463
464    #[test]
465    fn test_evaluation_report() {
466        let report = EvaluationReport {
467            success: false,
468            issues: vec![EvaluationIssue {
469                issue_type: IssueType::EmptyResults,
470                description: "No results found".to_string(),
471                severity: 0.9,
472            }],
473            suggestions: vec!["Try broader search pattern".to_string()],
474            score: 0.1,
475        };
476
477        assert!(!report.success);
478        assert_eq!(report.issues.len(), 1);
479        assert!(report.score < 0.5);
480    }
481}