diff --git a/dmtools-core/src/main/java/com/github/istin/dmtools/di/AIAgentsModule.java b/dmtools-core/src/main/java/com/github/istin/dmtools/di/AIAgentsModule.java index f3b28c92..648efa02 100644 --- a/dmtools-core/src/main/java/com/github/istin/dmtools/di/AIAgentsModule.java +++ b/dmtools-core/src/main/java/com/github/istin/dmtools/di/AIAgentsModule.java @@ -6,6 +6,11 @@ import com.github.istin.dmtools.atlassian.jira.BasicJiraClient; import com.github.istin.dmtools.context.ContextOrchestrator; import com.github.istin.dmtools.presentation.PresentationMakerOrchestrator; +import com.github.istin.dmtools.projectsetup.agent.FinalStatusDetectionAgent; +import com.github.istin.dmtools.projectsetup.agent.ProjectSetupAnalysisAgent; +import com.github.istin.dmtools.projectsetup.agent.StoryDescriptionWritingRulesAgent; +import com.github.istin.dmtools.projectsetup.agent.TestCaseWritingRulesAgent; +import com.github.istin.dmtools.projectsetup.agent.WorkflowAnalysisAgent; import com.github.istin.dmtools.prompt.IPromptTemplateReader; import com.github.istin.dmtools.search.ConfluenceSearchOrchestrator; import com.github.istin.dmtools.search.TrackerSearchOrchestrator; @@ -139,4 +144,29 @@ ToolSelectorAgent provideToolSelectorAgent() { MermaidDiagramGeneratorAgent provideMermaidDiagramGeneratorAgent() { return new MermaidDiagramGeneratorAgent(); } + + @Provides + FinalStatusDetectionAgent provideFinalStatusDetectionAgent() { + return new FinalStatusDetectionAgent(); + } + + @Provides + ProjectSetupAnalysisAgent provideProjectSetupAnalysisAgent() { + return new ProjectSetupAnalysisAgent(); + } + + @Provides + WorkflowAnalysisAgent provideWorkflowAnalysisAgent() { + return new WorkflowAnalysisAgent(); + } + + @Provides + StoryDescriptionWritingRulesAgent provideStoryDescriptionWritingRulesAgent() { + return new StoryDescriptionWritingRulesAgent(); + } + + @Provides + TestCaseWritingRulesAgent provideTestCaseWritingRulesAgent() { + return new TestCaseWritingRulesAgent(); + } } diff --git a/dmtools-core/src/main/java/com/github/istin/dmtools/di/FinalStatusDetectionAgentComponent.java b/dmtools-core/src/main/java/com/github/istin/dmtools/di/FinalStatusDetectionAgentComponent.java new file mode 100644 index 00000000..0a509267 --- /dev/null +++ b/dmtools-core/src/main/java/com/github/istin/dmtools/di/FinalStatusDetectionAgentComponent.java @@ -0,0 +1,12 @@ +package com.github.istin.dmtools.di; + +import com.github.istin.dmtools.projectsetup.agent.FinalStatusDetectionAgent; +import dagger.Component; + +import javax.inject.Singleton; + +@Singleton +@Component(modules = {ConfigurationModule.class, AIComponentsModule.class}) +public interface FinalStatusDetectionAgentComponent { + void inject(FinalStatusDetectionAgent finalStatusDetectionAgent); +} diff --git a/dmtools-core/src/main/java/com/github/istin/dmtools/di/ProjectSetupAnalysisAgentComponent.java b/dmtools-core/src/main/java/com/github/istin/dmtools/di/ProjectSetupAnalysisAgentComponent.java new file mode 100644 index 00000000..c49f362b --- /dev/null +++ b/dmtools-core/src/main/java/com/github/istin/dmtools/di/ProjectSetupAnalysisAgentComponent.java @@ -0,0 +1,12 @@ +package com.github.istin.dmtools.di; + +import com.github.istin.dmtools.projectsetup.agent.ProjectSetupAnalysisAgent; +import dagger.Component; + +import javax.inject.Singleton; + +@Singleton +@Component(modules = {ConfigurationModule.class, AIComponentsModule.class}) +public interface ProjectSetupAnalysisAgentComponent { + void inject(ProjectSetupAnalysisAgent projectSetupAnalysisAgent); +} diff --git a/dmtools-core/src/main/java/com/github/istin/dmtools/di/StoryDescriptionWritingRulesAgentComponent.java b/dmtools-core/src/main/java/com/github/istin/dmtools/di/StoryDescriptionWritingRulesAgentComponent.java new file mode 100644 index 00000000..4124f544 --- /dev/null +++ b/dmtools-core/src/main/java/com/github/istin/dmtools/di/StoryDescriptionWritingRulesAgentComponent.java @@ -0,0 +1,12 @@ +package com.github.istin.dmtools.di; + +import com.github.istin.dmtools.projectsetup.agent.StoryDescriptionWritingRulesAgent; +import dagger.Component; + +import javax.inject.Singleton; + +@Singleton +@Component(modules = {ConfigurationModule.class, AIComponentsModule.class}) +public interface StoryDescriptionWritingRulesAgentComponent { + void inject(StoryDescriptionWritingRulesAgent storyDescriptionWritingRulesAgent); +} diff --git a/dmtools-core/src/main/java/com/github/istin/dmtools/di/TestCaseWritingRulesAgentComponent.java b/dmtools-core/src/main/java/com/github/istin/dmtools/di/TestCaseWritingRulesAgentComponent.java new file mode 100644 index 00000000..a26ecd4c --- /dev/null +++ b/dmtools-core/src/main/java/com/github/istin/dmtools/di/TestCaseWritingRulesAgentComponent.java @@ -0,0 +1,12 @@ +package com.github.istin.dmtools.di; + +import com.github.istin.dmtools.projectsetup.agent.TestCaseWritingRulesAgent; +import dagger.Component; + +import javax.inject.Singleton; + +@Singleton +@Component(modules = {ConfigurationModule.class, AIComponentsModule.class}) +public interface TestCaseWritingRulesAgentComponent { + void inject(TestCaseWritingRulesAgent testCaseWritingRulesAgent); +} diff --git a/dmtools-core/src/main/java/com/github/istin/dmtools/di/WorkflowAnalysisAgentComponent.java b/dmtools-core/src/main/java/com/github/istin/dmtools/di/WorkflowAnalysisAgentComponent.java new file mode 100644 index 00000000..5b534829 --- /dev/null +++ b/dmtools-core/src/main/java/com/github/istin/dmtools/di/WorkflowAnalysisAgentComponent.java @@ -0,0 +1,12 @@ +package com.github.istin.dmtools.di; + +import com.github.istin.dmtools.projectsetup.agent.WorkflowAnalysisAgent; +import dagger.Component; + +import javax.inject.Singleton; + +@Singleton +@Component(modules = {ConfigurationModule.class, AIComponentsModule.class}) +public interface WorkflowAnalysisAgentComponent { + void inject(WorkflowAnalysisAgent workflowAnalysisAgent); +} diff --git a/dmtools-core/src/main/java/com/github/istin/dmtools/projectsetup/ProjectSetupAnalysisJob.java b/dmtools-core/src/main/java/com/github/istin/dmtools/projectsetup/ProjectSetupAnalysisJob.java new file mode 100644 index 00000000..a8991b71 --- /dev/null +++ b/dmtools-core/src/main/java/com/github/istin/dmtools/projectsetup/ProjectSetupAnalysisJob.java @@ -0,0 +1,266 @@ +package com.github.istin.dmtools.projectsetup; + +import com.github.istin.dmtools.atlassian.jira.JiraClient; +import com.github.istin.dmtools.common.model.ITicket; +import com.github.istin.dmtools.common.model.ToText; +import com.github.istin.dmtools.common.tracker.TrackerClient; +import com.github.istin.dmtools.di.ServerManagedIntegrationsModule; +import com.github.istin.dmtools.job.AbstractJob; +import com.github.istin.dmtools.projectsetup.agent.*; +import lombok.Getter; +import org.json.JSONArray; +import org.json.JSONObject; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import dagger.Component; + +public class ProjectSetupAnalysisJob extends AbstractJob { + + @Inject + TrackerClient trackerClient; + + @Inject + FinalStatusDetectionAgent finalStatusDetectionAgent; + + @Inject + ProjectSetupAnalysisAgent projectSetupAnalysisAgent; + + @Inject + WorkflowAnalysisAgent workflowAnalysisAgent; + + @Inject + StoryDescriptionWritingRulesAgent storyDescriptionWritingRulesAgent; + + @Inject + TestCaseWritingRulesAgent testCaseWritingRulesAgent; + + @Getter + @Inject + com.github.istin.dmtools.ai.AI ai; + + @Singleton + @Component(modules = {com.github.istin.dmtools.di.ConfigurationModule.class, + com.github.istin.dmtools.di.JiraModule.class, + com.github.istin.dmtools.di.AIComponentsModule.class, + com.github.istin.dmtools.di.ConfluenceModule.class, + com.github.istin.dmtools.di.AIAgentsModule.class}) + public interface ProjectSetupAnalysisJobComponent { + void inject(ProjectSetupAnalysisJob projectSetupAnalysisJob); + } + + @Singleton + @Component(modules = {ServerManagedIntegrationsModule.class, com.github.istin.dmtools.di.AIAgentsModule.class}) + public interface ServerManagedProjectSetupAnalysisJobComponent { + void inject(ProjectSetupAnalysisJob projectSetupAnalysisJob); + } + + public ProjectSetupAnalysisJob() { + } + + @Override + protected void initializeStandalone() { + DaggerProjectSetupAnalysisJob_ProjectSetupAnalysisJobComponent.create().inject(this); + } + + @Override + protected void initializeServerManaged(org.json.JSONObject resolvedIntegrations) { + try { + ServerManagedIntegrationsModule module = new ServerManagedIntegrationsModule(resolvedIntegrations); + ServerManagedProjectSetupAnalysisJobComponent component = + DaggerProjectSetupAnalysisJob_ServerManagedProjectSetupAnalysisJobComponent.builder() + .serverManagedIntegrationsModule(module) + .build(); + component.inject(this); + } catch (Exception e) { + throw new RuntimeException("Failed to initialize ProjectSetupAnalysisJob in server-managed mode", e); + } + } + + @Override + protected JSONObject executeJob(ProjectSetupAnalysisJobParams params) throws Exception { + String projectKey = params.getProjectKey(); + if (projectKey == null || projectKey.trim().isEmpty()) { + throw new IllegalArgumentException("projectKey is required"); + } + + // Step 1: Get workflow metadata for final status detection + String workflowMetadata = getWorkflowMetadata(projectKey); + + // Step 2: Detect final statuses + JSONArray finalStatuses = finalStatusDetectionAgent.run( + new FinalStatusDetectionAgent.Params(projectKey, workflowMetadata) + ); + + // Step 3: Get issue types and fields + // Cast TrackerClient to JiraClient to access getIssueTypes and getFields methods + JiraClient jiraClient = (JiraClient) trackerClient; + List issueTypes = jiraClient.getIssueTypes(projectKey); + String fieldsJson = jiraClient.getFields(projectKey); + + // Step 4: Analyze project setup + JSONObject projectSetupResult = projectSetupAnalysisAgent.run( + new ProjectSetupAnalysisAgent.Params( + projectKey, + new JSONArray(issueTypes).toString(), + fieldsJson + ) + ); + + // Step 5: Get completed tickets (last 50) + List completedTickets = getCompletedTickets(projectKey, finalStatuses); + String completedTicketsData = formatTicketsForAnalysis(completedTickets); + + // Step 6: Analyze workflow + JSONObject workflowAnalysisResult = workflowAnalysisAgent.run( + new WorkflowAnalysisAgent.Params( + projectKey, + finalStatuses, + completedTicketsData + ) + ); + + // Step 7: Extract story descriptions + String storyDescriptionsData = extractStoryDescriptions(completedTickets); + + // Step 8: Generate story description writing rules + JSONObject storyDescriptionRules = storyDescriptionWritingRulesAgent.run( + new StoryDescriptionWritingRulesAgent.Params( + projectKey, + storyDescriptionsData + ) + ); + + // Step 9: Extract test case data + String testCaseData = extractTestCaseData(completedTickets); + + // Step 10: Generate test case writing rules + JSONObject testCaseRules = testCaseWritingRulesAgent.run( + new TestCaseWritingRulesAgent.Params( + projectKey, + testCaseData + ) + ); + + // Step 11: Aggregate all results + JSONObject result = new JSONObject(); + result.put("projectKey", projectKey); + result.put("projectIssueTypes", projectSetupResult.optJSONArray("issueTypes") != null + ? projectSetupResult.getJSONArray("issueTypes") + : new JSONArray(issueTypes)); + result.put("projectFields", projectSetupResult.optJSONObject("fields") != null + ? projectSetupResult.getJSONObject("fields") + : new JSONObject().put("raw", fieldsJson)); + result.put("finalStatuses", finalStatuses); + result.put("workflowAnalysis", workflowAnalysisResult); + result.put("storyDescriptionRules", storyDescriptionRules); + result.put("testCaseRules", testCaseRules); + + return result; + } + + private String getWorkflowMetadata(String projectKey) throws Exception { + // Try to get workflow metadata from Jira API + // For now, return a placeholder - this would need to call Jira workflow API + try { + // Attempt to get workflow information + // This is a simplified approach - in production, you'd call the Jira workflow API + return "{\"projectKey\":\"" + projectKey + "\",\"note\":\"Workflow metadata retrieval needs Jira workflow API integration\"}"; + } catch (Exception e) { + return "{\"projectKey\":\"" + projectKey + "\",\"error\":\"" + e.getMessage() + "\"}"; + } + } + + private List getCompletedTickets(String projectKey, JSONArray finalStatuses) throws Exception { + // Build JQL query for tickets in final statuses + StringBuilder jql = new StringBuilder("project = ").append(projectKey); + + if (finalStatuses != null && finalStatuses.length() > 0) { + jql.append(" AND status IN ("); + for (int i = 0; i < finalStatuses.length(); i++) { + if (i > 0) jql.append(", "); + String status = finalStatuses.getString(i); + jql.append("\"").append(status).append("\""); + } + jql.append(")"); + } else { + // Fallback to common final statuses if detection failed + jql.append(" AND status IN (\"Done\", \"Closed\", \"Resolved\", \"Completed\")"); + } + + jql.append(" ORDER BY updated DESC"); + + // Get last 50 tickets + List allTickets = trackerClient.searchAndPerform( + jql.toString(), + trackerClient.getExtendedQueryFields() + ); + + return allTickets.stream() + .limit(50) + .collect(Collectors.toList()); + } + + private String formatTicketsForAnalysis(List tickets) { + List ticketTexts = new ArrayList<>(); + for (ITicket ticket : tickets) { + try { + ticketTexts.add(ticket.toText()); + } catch (Exception e) { + // Fallback to basic ticket info if toText() fails + try { + String title = ticket.getTicketTitle(); + String description = ticket.getTicketDescription(); + ticketTexts.add("Issue: " + ticket.getTicketKey() + "\nSummary: " + + (title != null ? title : "") + + "\nDescription: " + (description != null ? description : "")); + } catch (Exception ex) { + // If even fallback fails, just use the key + ticketTexts.add("Issue: " + ticket.getTicketKey()); + } + } + } + return String.join("\n\n---\n\n", ticketTexts); + } + + private String extractStoryDescriptions(List tickets) { + List descriptions = new ArrayList<>(); + for (ITicket ticket : tickets) { + try { + // Include all issue types, not just Story (per DMC-778) + String description = ticket.getTicketDescription(); + if (description != null && !description.trim().isEmpty()) { + descriptions.add("Issue: " + ticket.getTicketKey() + "\nType: " + ticket.getIssueType() + "\nDescription: " + description); + } + } catch (Exception e) { + // Skip tickets that fail to retrieve description + } + } + return String.join("\n\n---\n\n", descriptions); + } + + private String extractTestCaseData(List tickets) { + List testCaseData = new ArrayList<>(); + for (ITicket ticket : tickets) { + try { + // Include all issue types for test case analysis (per DMC-778) + String description = ticket.getTicketDescription(); + String summary = ticket.getTicketTitle(); + if ((description != null && !description.trim().isEmpty()) || + (summary != null && !summary.trim().isEmpty())) { + testCaseData.add("Issue: " + ticket.getTicketKey() + + "\nType: " + ticket.getIssueType() + + "\nSummary: " + (summary != null ? summary : "") + + "\nDescription: " + (description != null ? description : "")); + } + } catch (Exception e) { + // Skip tickets that fail to retrieve data + } + } + return String.join("\n\n---\n\n", testCaseData); + } +} diff --git a/dmtools-core/src/main/java/com/github/istin/dmtools/projectsetup/ProjectSetupAnalysisJobParams.java b/dmtools-core/src/main/java/com/github/istin/dmtools/projectsetup/ProjectSetupAnalysisJobParams.java new file mode 100644 index 00000000..b852c2c1 --- /dev/null +++ b/dmtools-core/src/main/java/com/github/istin/dmtools/projectsetup/ProjectSetupAnalysisJobParams.java @@ -0,0 +1,20 @@ +package com.github.istin.dmtools.projectsetup; + +import com.github.istin.dmtools.job.Params; +import com.google.gson.annotations.SerializedName; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +@Data +@EqualsAndHashCode(callSuper = false) +@NoArgsConstructor +@AllArgsConstructor +public class ProjectSetupAnalysisJobParams extends Params { + + public static final String PROJECT_KEY = "projectKey"; + + @SerializedName(PROJECT_KEY) + private String projectKey; +} diff --git a/dmtools-core/src/main/java/com/github/istin/dmtools/projectsetup/agent/FinalStatusDetectionAgent.java b/dmtools-core/src/main/java/com/github/istin/dmtools/projectsetup/agent/FinalStatusDetectionAgent.java new file mode 100644 index 00000000..76509a3a --- /dev/null +++ b/dmtools-core/src/main/java/com/github/istin/dmtools/projectsetup/agent/FinalStatusDetectionAgent.java @@ -0,0 +1,29 @@ +package com.github.istin.dmtools.projectsetup.agent; + +import com.github.istin.dmtools.ai.agent.AbstractSimpleAgent; +import com.github.istin.dmtools.ai.utils.AIResponseParser; +import com.github.istin.dmtools.di.DaggerFinalStatusDetectionAgentComponent; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.json.JSONArray; +import org.json.JSONObject; + +public class FinalStatusDetectionAgent extends AbstractSimpleAgent { + + @AllArgsConstructor + @Getter + public static class Params { + private String projectKey; + private String workflowMetadata; + } + + public FinalStatusDetectionAgent() { + super("agents/final_status_detection"); + DaggerFinalStatusDetectionAgentComponent.create().inject(this); + } + + @Override + public JSONArray transformAIResponse(Params params, String response) throws Exception { + return AIResponseParser.parseResponseAsJSONArray(response); + } +} diff --git a/dmtools-core/src/main/java/com/github/istin/dmtools/projectsetup/agent/ProjectSetupAnalysisAgent.java b/dmtools-core/src/main/java/com/github/istin/dmtools/projectsetup/agent/ProjectSetupAnalysisAgent.java new file mode 100644 index 00000000..18c1c206 --- /dev/null +++ b/dmtools-core/src/main/java/com/github/istin/dmtools/projectsetup/agent/ProjectSetupAnalysisAgent.java @@ -0,0 +1,29 @@ +package com.github.istin.dmtools.projectsetup.agent; + +import com.github.istin.dmtools.ai.agent.AbstractSimpleAgent; +import com.github.istin.dmtools.ai.utils.AIResponseParser; +import com.github.istin.dmtools.di.DaggerProjectSetupAnalysisAgentComponent; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.json.JSONObject; + +public class ProjectSetupAnalysisAgent extends AbstractSimpleAgent { + + @AllArgsConstructor + @Getter + public static class Params { + private String projectKey; + private String issueTypesJson; + private String fieldsJson; + } + + public ProjectSetupAnalysisAgent() { + super("agents/project_setup_analysis"); + DaggerProjectSetupAnalysisAgentComponent.create().inject(this); + } + + @Override + public JSONObject transformAIResponse(Params params, String response) throws Exception { + return AIResponseParser.parseResponseAsJSONObject(response); + } +} diff --git a/dmtools-core/src/main/java/com/github/istin/dmtools/projectsetup/agent/StoryDescriptionWritingRulesAgent.java b/dmtools-core/src/main/java/com/github/istin/dmtools/projectsetup/agent/StoryDescriptionWritingRulesAgent.java new file mode 100644 index 00000000..2d54c911 --- /dev/null +++ b/dmtools-core/src/main/java/com/github/istin/dmtools/projectsetup/agent/StoryDescriptionWritingRulesAgent.java @@ -0,0 +1,28 @@ +package com.github.istin.dmtools.projectsetup.agent; + +import com.github.istin.dmtools.ai.agent.AbstractSimpleAgent; +import com.github.istin.dmtools.ai.utils.AIResponseParser; +import com.github.istin.dmtools.di.DaggerStoryDescriptionWritingRulesAgentComponent; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.json.JSONObject; + +public class StoryDescriptionWritingRulesAgent extends AbstractSimpleAgent { + + @AllArgsConstructor + @Getter + public static class Params { + private String projectKey; + private String storyDescriptionsData; + } + + public StoryDescriptionWritingRulesAgent() { + super("agents/story_description_writing_rules"); + DaggerStoryDescriptionWritingRulesAgentComponent.create().inject(this); + } + + @Override + public JSONObject transformAIResponse(Params params, String response) throws Exception { + return AIResponseParser.parseResponseAsJSONObject(response); + } +} diff --git a/dmtools-core/src/main/java/com/github/istin/dmtools/projectsetup/agent/TestCaseWritingRulesAgent.java b/dmtools-core/src/main/java/com/github/istin/dmtools/projectsetup/agent/TestCaseWritingRulesAgent.java new file mode 100644 index 00000000..cfa71286 --- /dev/null +++ b/dmtools-core/src/main/java/com/github/istin/dmtools/projectsetup/agent/TestCaseWritingRulesAgent.java @@ -0,0 +1,28 @@ +package com.github.istin.dmtools.projectsetup.agent; + +import com.github.istin.dmtools.ai.agent.AbstractSimpleAgent; +import com.github.istin.dmtools.ai.utils.AIResponseParser; +import com.github.istin.dmtools.di.DaggerTestCaseWritingRulesAgentComponent; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.json.JSONObject; + +public class TestCaseWritingRulesAgent extends AbstractSimpleAgent { + + @AllArgsConstructor + @Getter + public static class Params { + private String projectKey; + private String testCaseData; + } + + public TestCaseWritingRulesAgent() { + super("agents/test_case_writing_rules"); + DaggerTestCaseWritingRulesAgentComponent.create().inject(this); + } + + @Override + public JSONObject transformAIResponse(Params params, String response) throws Exception { + return AIResponseParser.parseResponseAsJSONObject(response); + } +} diff --git a/dmtools-core/src/main/java/com/github/istin/dmtools/projectsetup/agent/WorkflowAnalysisAgent.java b/dmtools-core/src/main/java/com/github/istin/dmtools/projectsetup/agent/WorkflowAnalysisAgent.java new file mode 100644 index 00000000..24519661 --- /dev/null +++ b/dmtools-core/src/main/java/com/github/istin/dmtools/projectsetup/agent/WorkflowAnalysisAgent.java @@ -0,0 +1,30 @@ +package com.github.istin.dmtools.projectsetup.agent; + +import com.github.istin.dmtools.ai.agent.AbstractSimpleAgent; +import com.github.istin.dmtools.ai.utils.AIResponseParser; +import com.github.istin.dmtools.di.DaggerWorkflowAnalysisAgentComponent; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.json.JSONArray; +import org.json.JSONObject; + +public class WorkflowAnalysisAgent extends AbstractSimpleAgent { + + @AllArgsConstructor + @Getter + public static class Params { + private String projectKey; + private JSONArray finalStatuses; + private String completedTicketsData; + } + + public WorkflowAnalysisAgent() { + super("agents/workflow_analysis"); + DaggerWorkflowAnalysisAgentComponent.create().inject(this); + } + + @Override + public JSONObject transformAIResponse(Params params, String response) throws Exception { + return AIResponseParser.parseResponseAsJSONObject(response); + } +} diff --git a/dmtools-core/src/main/resources/ftl/prompts/agents/final_status_detection.xml b/dmtools-core/src/main/resources/ftl/prompts/agents/final_status_detection.xml new file mode 100644 index 00000000..8c4af99b --- /dev/null +++ b/dmtools-core/src/main/resources/ftl/prompts/agents/final_status_detection.xml @@ -0,0 +1,38 @@ + + + You're an expert in Jira workflow analysis specializing in identifying terminal workflow statuses. + + + Your task is to analyze Jira workflow metadata and identify all final/terminal statuses. + Final statuses are statuses that have no outgoing transitions - tickets in these statuses cannot move to any other status. + + Rules: + 1. Analyze the workflow metadata provided to identify all statuses + 2. For each status, determine if it has outgoing transitions + 3. Statuses with no outgoing transitions are final/terminal statuses + 4. Return only valid JSON format without any additional text or formatting + 5. Return a JSON array of status names (strings) + + + + ${global.projectKey} + + + ${global.workflowMetadata} + + + + + Return results as a JSON array of status name strings. + Example: ["Done", "Closed", "Resolved"] + The output must be in plain JSON format without any additional text, formatting, or markdown. + Don't use ```, json markdowns. + + + + + Identify final statuses for a project with workflow metadata showing statuses: To Do, In Progress, Done, Closed + ["Done", "Closed"] + + + diff --git a/dmtools-core/src/main/resources/ftl/prompts/agents/project_setup_analysis.xml b/dmtools-core/src/main/resources/ftl/prompts/agents/project_setup_analysis.xml new file mode 100644 index 00000000..f3e9394a --- /dev/null +++ b/dmtools-core/src/main/resources/ftl/prompts/agents/project_setup_analysis.xml @@ -0,0 +1,42 @@ + + + You're an expert in Jira project configuration analysis specializing in understanding project structure and field definitions. + + + Your task is to analyze Jira project issue types and fields to provide structured information about the project setup. + + Rules: + 1. Analyze the issue types JSON to extract all issue types with their properties + 2. Analyze the fields JSON to extract all fields (including custom fields) relevant for agent understanding + 3. Structure the output to include issue types and fields information + 4. Return only valid JSON format without any additional text or formatting + + + + ${global.projectKey} + + + ${global.issueTypesJson} + + + ${global.fieldsJson} + + + + + Return results as a JSON object with the following structure: + { + "issueTypes": [array of issue type objects with name, id, and other properties], + "fields": {object containing field information} + } + The output must be in plain JSON format without any additional text, formatting, or markdown. + Don't use ```, json markdowns. + + + + + Analyze project setup with issue types: Story, Bug, Task and various fields + {"issueTypes": [{"name": "Story", "id": "10001"}, {"name": "Bug", "id": "10002"}, {"name": "Task", "id": "10003"}], "fields": {"summary": "Summary", "description": "Description", "customfield_10010": "Story Points"}} + + + diff --git a/dmtools-core/src/main/resources/ftl/prompts/agents/story_description_writing_rules.xml b/dmtools-core/src/main/resources/ftl/prompts/agents/story_description_writing_rules.xml new file mode 100644 index 00000000..fe2e3f1c --- /dev/null +++ b/dmtools-core/src/main/resources/ftl/prompts/agents/story_description_writing_rules.xml @@ -0,0 +1,53 @@ + + + You're an expert in technical writing and documentation analysis specializing in extracting writing patterns and conventions from existing content. + + + Your task is to analyze story descriptions from completed tickets to identify common patterns, structures, and formats used in story descriptions. + Generate comprehensive writing rules that describe how story descriptions are written in this specific project. + + Rules: + 1. Analyze all story descriptions provided to identify common patterns + 2. Extract writing conventions specific to this project + 3. Identify template structures (e.g., business context, user stories, acceptance criteria sections) + 4. Identify statistical patterns (common phrases, average length, field usage frequency) + 5. Provide representative examples that show common patterns + 6. Generate rules that combine template structure + statistical analysis + examples + 7. Return only valid JSON format without any additional text or formatting + + + + ${global.projectKey} + + + ${global.storyDescriptionsData} + + + + + Return results as a JSON object with the following structure: + { + "templateStructure": { + "sections": [array of common sections found], + "requiredFields": [array of required fields], + "optionalFields": [array of optional fields] + }, + "statisticalPatterns": { + "averageLength": number, + "commonPhrases": [array of common phrases], + "fieldUsageFrequency": {object mapping field names to usage frequency} + }, + "examples": [array of representative examples showing common patterns], + "rules": "Summary of writing rules and conventions" + } + The output must be in plain JSON format without any additional text, formatting, or markdown. + Don't use ```, json markdowns. + + + + + Generate writing rules from story descriptions + {"templateStructure": {"sections": ["Business Context", "User Story", "Acceptance Criteria"], "requiredFields": ["summary", "description"], "optionalFields": ["acceptanceCriteria"]}, "statisticalPatterns": {"averageLength": 500, "commonPhrases": ["As a", "I want", "So that"], "fieldUsageFrequency": {"summary": 1.0, "description": 0.95}}, "examples": ["Example story 1", "Example story 2"], "rules": "Stories should include business context, user story format, and acceptance criteria"} + + + diff --git a/dmtools-core/src/main/resources/ftl/prompts/agents/test_case_writing_rules.xml b/dmtools-core/src/main/resources/ftl/prompts/agents/test_case_writing_rules.xml new file mode 100644 index 00000000..d2d84ec4 --- /dev/null +++ b/dmtools-core/src/main/resources/ftl/prompts/agents/test_case_writing_rules.xml @@ -0,0 +1,55 @@ + + + You're an expert in test case documentation analysis specializing in extracting writing patterns and conventions for test case descriptions. + + + Your task is to analyze test case information from completed tickets (all issue types) to identify common patterns in how test cases are structured and documented. + Generate comprehensive writing rules that describe how test cases are written in this specific project. + + Rules: + 1. Analyze all test case data provided (from all issue types, not just Test Case type) + 2. Identify common patterns in how test cases are structured and documented + 3. Extract writing conventions for test case descriptions + 4. Identify template structures (e.g., test scenarios, expected results, test case organization) + 5. Identify statistical patterns (common phrases, average length, structure patterns) + 6. Provide representative examples that show common patterns + 7. Generate rules that combine template structure + statistical analysis + examples + 8. Return only valid JSON format without any additional text or formatting + + + + ${global.projectKey} + + + ${global.testCaseData} + + + + + Return results as a JSON object with the following structure: + { + "templateStructure": { + "sections": [array of common sections found], + "requiredFields": [array of required fields], + "optionalFields": [array of optional fields], + "organizationPattern": "Description of how test cases are organized" + }, + "statisticalPatterns": { + "averageLength": number, + "commonPhrases": [array of common phrases], + "structurePatterns": [array of common structure patterns] + }, + "examples": [array of representative examples showing common patterns], + "rules": "Summary of writing rules and conventions for test cases" + } + The output must be in plain JSON format without any additional text, formatting, or markdown. + Don't use ```, json markdowns. + + + + + Generate writing rules from test case data + {"templateStructure": {"sections": ["Test Scenario", "Steps", "Expected Result"], "requiredFields": ["summary", "description"], "optionalFields": ["preconditions"]}, "statisticalPatterns": {"averageLength": 300, "commonPhrases": ["Given", "When", "Then"], "structurePatterns": ["Given-When-Then format"]}, "examples": ["Example test case 1", "Example test case 2"], "rules": "Test cases should follow Given-When-Then format with clear steps and expected results"} + + + diff --git a/dmtools-core/src/main/resources/ftl/prompts/agents/workflow_analysis.xml b/dmtools-core/src/main/resources/ftl/prompts/agents/workflow_analysis.xml new file mode 100644 index 00000000..65f1a678 --- /dev/null +++ b/dmtools-core/src/main/resources/ftl/prompts/agents/workflow_analysis.xml @@ -0,0 +1,45 @@ + + + You're an expert in Jira workflow analysis specializing in understanding ticket lifecycle patterns and workflow transitions. + + + Your task is to analyze completed tickets to understand workflow patterns, transitions, and how tickets move through workflows to completion. + + Rules: + 1. Analyze the completed tickets data to identify patterns in workflow transitions + 2. Identify common paths tickets take to reach final statuses + 3. Note any patterns in status transitions + 4. Provide a summary of workflow analysis including identified final statuses + 5. Return only valid JSON format without any additional text or formatting + + + + ${global.projectKey} + + + ${global.finalStatuses} + + + ${global.completedTicketsData} + + + + + Return results as a JSON object with the following structure: + { + "summary": "Summary of workflow analysis", + "finalStatuses": [array of final status names], + "commonTransitions": [array of common transition patterns], + "patterns": "Description of identified patterns" + } + The output must be in plain JSON format without any additional text, formatting, or markdown. + Don't use ```, json markdowns. + + + + + Analyze workflow patterns from completed tickets + {"summary": "Tickets typically move from To Do -> In Progress -> Testing -> Done", "finalStatuses": ["Done"], "commonTransitions": ["To Do -> In Progress", "In Progress -> Testing", "Testing -> Done"], "patterns": "Most tickets follow a linear progression through statuses"} + + + diff --git a/dmtools-core/src/test/java/com/github/istin/dmtools/projectsetup/ProjectSetupAnalysisJobParamsTest.java b/dmtools-core/src/test/java/com/github/istin/dmtools/projectsetup/ProjectSetupAnalysisJobParamsTest.java new file mode 100644 index 00000000..4bf62925 --- /dev/null +++ b/dmtools-core/src/test/java/com/github/istin/dmtools/projectsetup/ProjectSetupAnalysisJobParamsTest.java @@ -0,0 +1,28 @@ +package com.github.istin.dmtools.projectsetup; + +import org.junit.Test; + +import static org.junit.Assert.*; + +public class ProjectSetupAnalysisJobParamsTest { + + @Test + public void testDefaultConstructor() { + ProjectSetupAnalysisJobParams params = new ProjectSetupAnalysisJobParams(); + assertNull(params.getProjectKey()); + } + + @Test + public void testAllArgsConstructor() { + ProjectSetupAnalysisJobParams params = new ProjectSetupAnalysisJobParams(); + params.setProjectKey("TEST"); + assertEquals("TEST", params.getProjectKey()); + } + + @Test + public void testProjectKeySetterAndGetter() { + ProjectSetupAnalysisJobParams params = new ProjectSetupAnalysisJobParams(); + params.setProjectKey("PROJ"); + assertEquals("PROJ", params.getProjectKey()); + } +} diff --git a/dmtools-core/src/test/java/com/github/istin/dmtools/projectsetup/ProjectSetupAnalysisJobTest.java b/dmtools-core/src/test/java/com/github/istin/dmtools/projectsetup/ProjectSetupAnalysisJobTest.java new file mode 100644 index 00000000..291dacad --- /dev/null +++ b/dmtools-core/src/test/java/com/github/istin/dmtools/projectsetup/ProjectSetupAnalysisJobTest.java @@ -0,0 +1,150 @@ +package com.github.istin.dmtools.projectsetup; + +import com.github.istin.dmtools.atlassian.jira.JiraClient; +import com.github.istin.dmtools.atlassian.jira.model.IssueType; +import com.github.istin.dmtools.common.model.ITicket; +import com.github.istin.dmtools.common.tracker.TrackerClient; +import com.github.istin.dmtools.projectsetup.agent.*; +import org.junit.Before; +import org.junit.Test; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +public class ProjectSetupAnalysisJobTest { + + private ProjectSetupAnalysisJob job; + private TrackerClient mockTrackerClient; + private JiraClient mockJiraClient; + private FinalStatusDetectionAgent mockFinalStatusDetectionAgent; + private ProjectSetupAnalysisAgent mockProjectSetupAnalysisAgent; + private WorkflowAnalysisAgent mockWorkflowAnalysisAgent; + private StoryDescriptionWritingRulesAgent mockStoryDescriptionWritingRulesAgent; + private TestCaseWritingRulesAgent mockTestCaseWritingRulesAgent; + + @Before + public void setUp() throws Exception { + job = new ProjectSetupAnalysisJob(); + + // Use JiraClient mock as the trackerClient since ProjectSetupAnalysisJob casts it internally + mockJiraClient = mock(JiraClient.class); + mockFinalStatusDetectionAgent = mock(FinalStatusDetectionAgent.class); + mockProjectSetupAnalysisAgent = mock(ProjectSetupAnalysisAgent.class); + mockWorkflowAnalysisAgent = mock(WorkflowAnalysisAgent.class); + mockStoryDescriptionWritingRulesAgent = mock(StoryDescriptionWritingRulesAgent.class); + mockTestCaseWritingRulesAgent = mock(TestCaseWritingRulesAgent.class); + + // Use reflection to inject mocks + // Inject mockJiraClient as trackerClient (since ProjectSetupAnalysisJob casts trackerClient to JiraClient) + java.lang.reflect.Field trackerField = ProjectSetupAnalysisJob.class.getDeclaredField("trackerClient"); + trackerField.setAccessible(true); + trackerField.set(job, mockJiraClient); + + java.lang.reflect.Field finalStatusField = ProjectSetupAnalysisJob.class.getDeclaredField("finalStatusDetectionAgent"); + finalStatusField.setAccessible(true); + finalStatusField.set(job, mockFinalStatusDetectionAgent); + + java.lang.reflect.Field projectSetupField = ProjectSetupAnalysisJob.class.getDeclaredField("projectSetupAnalysisAgent"); + projectSetupField.setAccessible(true); + projectSetupField.set(job, mockProjectSetupAnalysisAgent); + + java.lang.reflect.Field workflowField = ProjectSetupAnalysisJob.class.getDeclaredField("workflowAnalysisAgent"); + workflowField.setAccessible(true); + workflowField.set(job, mockWorkflowAnalysisAgent); + + java.lang.reflect.Field storyField = ProjectSetupAnalysisJob.class.getDeclaredField("storyDescriptionWritingRulesAgent"); + storyField.setAccessible(true); + storyField.set(job, mockStoryDescriptionWritingRulesAgent); + + java.lang.reflect.Field testCaseField = ProjectSetupAnalysisJob.class.getDeclaredField("testCaseWritingRulesAgent"); + testCaseField.setAccessible(true); + testCaseField.set(job, mockTestCaseWritingRulesAgent); + } + + @Test + public void testExecuteJobWithValidParams() throws Exception { + // Setup + ProjectSetupAnalysisJobParams params = new ProjectSetupAnalysisJobParams(); + params.setProjectKey("TEST"); + + List issueTypes = new ArrayList<>(); + org.json.JSONObject storyTypeJson = new org.json.JSONObject(); + storyTypeJson.put("name", "Story"); + storyTypeJson.put("id", "10001"); + IssueType storyType = new IssueType(storyTypeJson); + issueTypes.add(storyType); + + JSONArray finalStatuses = new JSONArray(); + finalStatuses.put("Done"); + finalStatuses.put("Closed"); + + JSONObject projectSetupResult = new JSONObject(); + projectSetupResult.put("issueTypes", new JSONArray()); + projectSetupResult.put("fields", new JSONObject()); + + JSONObject workflowResult = new JSONObject(); + workflowResult.put("summary", "Workflow analysis"); + + JSONObject storyRules = new JSONObject(); + storyRules.put("rules", "Story writing rules"); + + JSONObject testCaseRules = new JSONObject(); + testCaseRules.put("rules", "Test case writing rules"); + + @SuppressWarnings("unchecked") + List completedTickets = new ArrayList<>(); + + // Mock behavior + when(mockJiraClient.getIssueTypes("TEST")).thenReturn(issueTypes); + when(mockJiraClient.getFields("TEST")).thenReturn("{\"fields\":[]}"); + when(mockFinalStatusDetectionAgent.run(any(FinalStatusDetectionAgent.Params.class))).thenReturn(finalStatuses); + when(mockProjectSetupAnalysisAgent.run(any(ProjectSetupAnalysisAgent.Params.class))).thenReturn(projectSetupResult); + doReturn(completedTickets).when(mockJiraClient).searchAndPerform(anyString(), any(String[].class)); + when(mockJiraClient.getExtendedQueryFields()).thenReturn(new String[]{"summary", "description"}); + when(mockWorkflowAnalysisAgent.run(any(WorkflowAnalysisAgent.Params.class))).thenReturn(workflowResult); + when(mockStoryDescriptionWritingRulesAgent.run(any(StoryDescriptionWritingRulesAgent.Params.class))).thenReturn(storyRules); + when(mockTestCaseWritingRulesAgent.run(any(TestCaseWritingRulesAgent.Params.class))).thenReturn(testCaseRules); + + // Execute + JSONObject result = job.executeJob(params); + + // Verify + assertNotNull(result); + assertEquals("TEST", result.getString("projectKey")); + assertNotNull(result.get("finalStatuses")); + assertNotNull(result.get("workflowAnalysis")); + assertNotNull(result.get("storyDescriptionRules")); + assertNotNull(result.get("testCaseRules")); + + verify(mockJiraClient).getIssueTypes("TEST"); + verify(mockJiraClient).getFields("TEST"); + verify(mockFinalStatusDetectionAgent).run(any(FinalStatusDetectionAgent.Params.class)); + verify(mockProjectSetupAnalysisAgent).run(any(ProjectSetupAnalysisAgent.Params.class)); + verify(mockWorkflowAnalysisAgent).run(any(WorkflowAnalysisAgent.Params.class)); + verify(mockStoryDescriptionWritingRulesAgent).run(any(StoryDescriptionWritingRulesAgent.Params.class)); + verify(mockTestCaseWritingRulesAgent).run(any(TestCaseWritingRulesAgent.Params.class)); + } + + @Test(expected = IllegalArgumentException.class) + public void testExecuteJobWithNullProjectKey() throws Exception { + ProjectSetupAnalysisJobParams params = new ProjectSetupAnalysisJobParams(); + params.setProjectKey(null); + + job.executeJob(params); + } + + @Test(expected = IllegalArgumentException.class) + public void testExecuteJobWithEmptyProjectKey() throws Exception { + ProjectSetupAnalysisJobParams params = new ProjectSetupAnalysisJobParams(); + params.setProjectKey(""); + + job.executeJob(params); + } +} diff --git a/dmtools-core/src/test/java/com/github/istin/dmtools/projectsetup/agent/FinalStatusDetectionAgentTest.java b/dmtools-core/src/test/java/com/github/istin/dmtools/projectsetup/agent/FinalStatusDetectionAgentTest.java new file mode 100644 index 00000000..c632dde4 --- /dev/null +++ b/dmtools-core/src/test/java/com/github/istin/dmtools/projectsetup/agent/FinalStatusDetectionAgentTest.java @@ -0,0 +1,58 @@ +package com.github.istin.dmtools.projectsetup.agent; + +import com.github.istin.dmtools.ai.AI; +import com.github.istin.dmtools.ai.utils.AIResponseParser; +import com.github.istin.dmtools.prompt.IPromptTemplateReader; +import org.junit.Before; +import org.junit.Test; +import org.json.JSONArray; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +public class FinalStatusDetectionAgentTest { + + private FinalStatusDetectionAgent agent; + private AI mockAI; + private IPromptTemplateReader mockPromptReader; + + @Before + public void setUp() throws Exception { + agent = new FinalStatusDetectionAgent(); + + mockAI = mock(AI.class); + mockPromptReader = mock(IPromptTemplateReader.class); + + // Use reflection to inject mocks + java.lang.reflect.Field aiField = FinalStatusDetectionAgent.class.getSuperclass().getDeclaredField("ai"); + aiField.setAccessible(true); + aiField.set(agent, mockAI); + + java.lang.reflect.Field promptField = FinalStatusDetectionAgent.class.getSuperclass().getDeclaredField("promptTemplateReader"); + promptField.setAccessible(true); + promptField.set(agent, mockPromptReader); + } + + @Test + public void testTransformAIResponse() throws Exception { + String aiResponse = "[\"Done\", \"Closed\", \"Resolved\"]"; + FinalStatusDetectionAgent.Params params = new FinalStatusDetectionAgent.Params("TEST", "{}"); + + JSONArray result = agent.transformAIResponse(params, aiResponse); + + assertNotNull(result); + assertEquals(3, result.length()); + assertEquals("Done", result.getString(0)); + assertEquals("Closed", result.getString(1)); + assertEquals("Resolved", result.getString(2)); + } + + @Test + public void testParamsConstructor() { + FinalStatusDetectionAgent.Params params = new FinalStatusDetectionAgent.Params("TEST", "metadata"); + assertEquals("TEST", params.getProjectKey()); + assertEquals("metadata", params.getWorkflowMetadata()); + } +} diff --git a/dmtools-server/src/main/java/com/github/istin/dmtools/server/JobService.java b/dmtools-server/src/main/java/com/github/istin/dmtools/server/JobService.java index 08eb5e67..04acb896 100644 --- a/dmtools-server/src/main/java/com/github/istin/dmtools/server/JobService.java +++ b/dmtools-server/src/main/java/com/github/istin/dmtools/server/JobService.java @@ -16,6 +16,7 @@ import com.github.istin.dmtools.job.JobParams; import com.github.istin.dmtools.job.Params; import com.github.istin.dmtools.presale.PreSaleSupport; +import com.github.istin.dmtools.projectsetup.ProjectSetupAnalysisJob; import com.github.istin.dmtools.qa.TestCasesGenerator; import com.github.istin.dmtools.report.productivity.BAProductivityReport; import com.github.istin.dmtools.report.productivity.DevProductivityReport; @@ -55,7 +56,8 @@ public class JobService { new SourceCodeCommitTrackerSyncJob(), new UserStoryGenerator(), new UnitTestsGenerator(), - new CommitsTriage() + new CommitsTriage(), + new ProjectSetupAnalysisJob() ); public void executeJob(JobParams jobParams) throws Exception {