syncable_cli/agent/tools/platform/
analyze_project.rs1use rig::completion::ToolDefinition;
7use rig::tool::Tool;
8use serde::{Deserialize, Serialize};
9use serde_json::json;
10use std::path::Path;
11
12use crate::agent::tools::error::{ErrorCategory, format_error_for_llm};
13use crate::analyzer::discover_dockerfiles_for_deployment;
14
15#[derive(Debug, Deserialize)]
17pub struct AnalyzeProjectArgs {
18 #[serde(default = "default_project_path")]
20 pub project_path: String,
21}
22
23fn default_project_path() -> String {
24 ".".to_string()
25}
26
27#[derive(Debug, thiserror::Error)]
29#[error("Analyze project error: {0}")]
30pub struct AnalyzeProjectError(String);
31
32#[derive(Debug, Clone, Serialize, Deserialize, Default)]
37pub struct AnalyzeProjectTool;
38
39impl AnalyzeProjectTool {
40 pub fn new() -> Self {
42 Self
43 }
44}
45
46impl Tool for AnalyzeProjectTool {
47 const NAME: &'static str = "analyze_project";
48
49 type Error = AnalyzeProjectError;
50 type Args = AnalyzeProjectArgs;
51 type Output = String;
52
53 async fn definition(&self, _prompt: String) -> ToolDefinition {
54 ToolDefinition {
55 name: Self::NAME.to_string(),
56 description: r#"Analyze a project directory to discover Dockerfiles and build configurations for deployment.
57
58Before deploying, use this tool to understand what can be deployed from a project.
59
60**What it detects:**
61- Dockerfiles and their variants (Dockerfile.dev, Dockerfile.prod, etc.)
62- Build context paths for each Dockerfile
63- Exposed ports from EXPOSE instructions or inferred from base images
64- Multi-stage build configurations
65- Suggested service names based on directory structure
66
67**Parameters:**
68- project_path: Path to the project directory (defaults to ".")
69
70**Use Cases:**
71- Before creating a deployment config, analyze the project structure
72- Understand what services can be deployed from a monorepo
73- Find the correct Dockerfile and build context for deployment
74
75**Returns:**
76- dockerfiles: Array of discovered Dockerfiles with deployment metadata
77- summary: Human-readable summary of what was found"#
78 .to_string(),
79 parameters: json!({
80 "type": "object",
81 "properties": {
82 "project_path": {
83 "type": "string",
84 "description": "Path to the project directory to analyze (defaults to current directory)",
85 "default": "."
86 }
87 },
88 "required": []
89 }),
90 }
91 }
92
93 async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
94 let project_path = Path::new(&args.project_path);
95
96 if !project_path.exists() {
98 return Ok(format_error_for_llm(
99 "analyze_project",
100 ErrorCategory::FileNotFound,
101 &format!("Project path does not exist: {}", args.project_path),
102 Some(vec![
103 "Check that the path is correct",
104 "Use an absolute path or path relative to current directory",
105 ]),
106 ));
107 }
108
109 if !project_path.is_dir() {
110 return Ok(format_error_for_llm(
111 "analyze_project",
112 ErrorCategory::ValidationFailed,
113 &format!("Path is not a directory: {}", args.project_path),
114 Some(vec!["Provide a directory path, not a file path"]),
115 ));
116 }
117
118 match discover_dockerfiles_for_deployment(project_path) {
120 Ok(dockerfiles) => {
121 let dockerfile_count = dockerfiles.len();
122
123 let dockerfile_data: Vec<serde_json::Value> = dockerfiles
125 .into_iter()
126 .map(|df| {
127 json!({
128 "path": df.path.display().to_string(),
129 "build_context": df.build_context,
130 "suggested_service_name": df.suggested_service_name,
131 "suggested_port": df.suggested_port,
132 "base_image": df.base_image,
133 "is_multistage": df.is_multistage,
134 "environment": df.environment,
135 })
136 })
137 .collect();
138
139 let summary = if dockerfile_count == 0 {
140 "No Dockerfiles found in this project. You may need to create a Dockerfile before deploying.".to_string()
141 } else {
142 format!(
143 "Found {} Dockerfile{} suitable for deployment",
144 dockerfile_count,
145 if dockerfile_count == 1 { "" } else { "s" }
146 )
147 };
148
149 let result = json!({
150 "success": true,
151 "project_path": args.project_path,
152 "dockerfiles": dockerfile_data,
153 "dockerfile_count": dockerfile_count,
154 "summary": summary,
155 "next_steps": if dockerfile_count > 0 {
156 vec![
157 "Use analyze_codebase for deeper analysis of build requirements and environment variables",
158 "Use list_deployment_capabilities to see available deployment targets",
159 "Use create_deployment_config to create a deployment configuration"
160 ]
161 } else {
162 vec![
163 "Use analyze_codebase to understand the project's technology stack and recommended Dockerfile base image",
164 "Create a Dockerfile for your application",
165 "Consider using a multi-stage build for smaller images"
166 ]
167 }
168 });
169
170 serde_json::to_string_pretty(&result)
171 .map_err(|e| AnalyzeProjectError(format!("Failed to serialize: {}", e)))
172 }
173 Err(e) => Ok(format_error_for_llm(
174 "analyze_project",
175 ErrorCategory::InternalError,
176 &format!("Failed to analyze project: {}", e),
177 Some(vec![
178 "Check that you have read permissions for the project directory",
179 "Ensure the path is accessible",
180 ]),
181 )),
182 }
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
191 fn test_tool_name() {
192 assert_eq!(AnalyzeProjectTool::NAME, "analyze_project");
193 }
194
195 #[test]
196 fn test_tool_creation() {
197 let tool = AnalyzeProjectTool::new();
198 assert!(format!("{:?}", tool).contains("AnalyzeProjectTool"));
199 }
200
201 #[test]
202 fn test_default_project_path() {
203 assert_eq!(default_project_path(), ".");
204 }
205}