Skip to content

Commit afd0dae

Browse files
authored
Merge pull request #82 from SentriusLLC/copilot/fix-81
Wire up JIRA capabilities for AI agent discovery and integration
2 parents 781b32f + b02a9fa commit afd0dae

File tree

86 files changed

+2771
-999
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

86 files changed

+2771
-999
lines changed

.local.env

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
SENTRIUS_VERSION=1.1.261
2-
SENTRIUS_SSH_VERSION=1.1.40
3-
SENTRIUS_KEYCLOAK_VERSION=1.1.52
4-
SENTRIUS_AGENT_VERSION=1.1.39
5-
SENTRIUS_AI_AGENT_VERSION=1.1.148
6-
LLMPROXY_VERSION=1.0.53
7-
LAUNCHER_VERSION=1.0.73
8-
AGENTPROXY_VERSION=1.0.74
1+
SENTRIUS_VERSION=1.1.325
2+
SENTRIUS_SSH_VERSION=1.1.41
3+
SENTRIUS_KEYCLOAK_VERSION=1.1.53
4+
SENTRIUS_AGENT_VERSION=1.1.42
5+
SENTRIUS_AI_AGENT_VERSION=1.1.263
6+
LLMPROXY_VERSION=1.0.78
7+
LAUNCHER_VERSION=1.0.82
8+
AGENTPROXY_VERSION=1.0.75

.local.env.bak

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
SENTRIUS_VERSION=1.1.261
2-
SENTRIUS_SSH_VERSION=1.1.40
3-
SENTRIUS_KEYCLOAK_VERSION=1.1.52
4-
SENTRIUS_AGENT_VERSION=1.1.39
5-
SENTRIUS_AI_AGENT_VERSION=1.1.148
6-
LLMPROXY_VERSION=1.0.53
7-
LAUNCHER_VERSION=1.0.73
8-
AGENTPROXY_VERSION=1.0.74
1+
SENTRIUS_VERSION=1.1.325
2+
SENTRIUS_SSH_VERSION=1.1.41
3+
SENTRIUS_KEYCLOAK_VERSION=1.1.53
4+
SENTRIUS_AGENT_VERSION=1.1.42
5+
SENTRIUS_AI_AGENT_VERSION=1.1.263
6+
LLMPROXY_VERSION=1.0.78
7+
LAUNCHER_VERSION=1.0.82
8+
AGENTPROXY_VERSION=1.0.75

agent-launcher/src/main/java/io/sentrius/agent/launcher/api/AgentLauncherController.java

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,19 @@
2323
@Slf4j
2424
@RestController
2525
@RequestMapping(ApiPaths.API_V1 + "/agent/launcher")
26-
public class AgentLauncherController {
26+
public class AgentLauncherController {
2727
private final PodLauncherService podLauncherService;
2828
private final KeycloakService keycloakService;
2929

30-
public AgentLauncherController(
31-
PodLauncherService podLauncherService, KeycloakService keycloakService) {
30+
protected AgentLauncherController(
31+
PodLauncherService podLauncherService, KeycloakService keycloakService
32+
) {
33+
3234
this.podLauncherService = podLauncherService;
3335
this.keycloakService = keycloakService;
3436
}
3537

38+
3639
@PostMapping("/create")
3740
@LimitAccess(applicationAccess = {ApplicationAccessEnum.CAN_MANAGE_APPLICATION})
3841
public ResponseEntity<?> createPod(
@@ -49,6 +52,9 @@ public ResponseEntity<?> createPod(
4952
return ResponseEntity.status(HttpStatus.SC_UNAUTHORIZED).body("Invalid Keycloak token");
5053
}
5154

55+
// create the user, assign the policy
56+
57+
5258
podLauncherService.launchAgentPod(agent);
5359

5460
return ResponseEntity.ok(Map.of("status", "success"));
@@ -65,9 +71,9 @@ public ResponseEntity<String> deleteAgent(@RequestParam(name="agentId") String a
6571
}
6672

6773
@GetMapping("/status")
68-
public ResponseEntity<String> getAgentStatus(@RequestParam(name="agentId") String agentId) {
74+
public ResponseEntity<?> getAgentStatus(@RequestParam(name="agentId") String agentId) {
6975
try {
70-
return ResponseEntity.ok(podLauncherService.statusById(agentId) );
76+
return ResponseEntity.ok(Map.of("status", podLauncherService.statusById(agentId)) );
7177
} catch (Exception e) {
7278
log.error("Status failed", e);
7379
return ResponseEntity.status(500).body("Status retrieval failed");

agent-launcher/src/main/java/io/sentrius/agent/launcher/service/PodLauncherService.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,12 @@ public V1Pod launchAgentPod(AgentRegistrationDTO agent) throws Exception {
178178
List<String> argList = new ArrayList<>();
179179
argList.add("--spring.config.location=file:/config/agent.properties");
180180
argList.add("--agent.namePrefix=" + agentId);
181+
argList.add("--agent.clientId=" + agent.getClientId());
181182
argList.add("--agent.listen.websocket=true");
182183
argList.add("--agent.callback.url=" + constructedCallbackUrl);
184+
if (agent.getAgentPolicyId() != null && !agent.getAgentPolicyId().isEmpty()) {
185+
argList.add("--agent.ai.policy.id=" + agent.getAgentPolicyId());
186+
}
183187
if (agent.getAgentContextId() != null && !agent.getAgentContextId().isEmpty()) {
184188
argList.add("--agent.ai.context.db.id=" + agent.getAgentContextId());
185189
}else {
@@ -215,8 +219,8 @@ public V1Pod launchAgentPod(AgentRegistrationDTO agent) throws Exception {
215219
.args(argList)
216220
.resources(new V1ResourceRequirements()
217221
.limits(Map.of(
218-
"cpu", Quantity.fromString("1000m"),
219-
"memory", Quantity.fromString("1Gi")
222+
"cpu", Quantity.fromString("2000m"),
223+
"memory", Quantity.fromString("2Gi")
220224
)))
221225
.volumeMounts(List.of(
222226
new V1VolumeMount()
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# JIRA Integration Architecture
2+
3+
## Overview
4+
5+
The AI Agent module can discover and use JIRA capabilities from the dataplane module without having a direct dependency on it. This maintains a clean separation of concerns where the ai-agent focuses on AI functionality while the dataplane handles data access and integrations.
6+
7+
## Architecture
8+
9+
```
10+
ai-agent module (no direct dependency on dataplane)
11+
├── VerbRegistry - discovers verbs from both modules
12+
│ ├── Scans: "io.sentrius.agent.analysis.agents.verbs"
13+
│ └── Scans: "io.sentrius.sso.core.integrations.ticketing"
14+
└── Gets JIRA verbs from Spring ApplicationContext
15+
16+
dataplane module (contains JIRA implementation)
17+
├── JiraVerbService - provides JIRA operations as @Verb methods
18+
│ ├── searchForTickets(@Verb)
19+
│ ├── assignTicket(@Verb)
20+
│ ├── updateTicket(@Verb)
21+
│ └── isJiraAvailable(@Verb)
22+
└── JiraService - actual JIRA integration logic
23+
```
24+
25+
## How It Works
26+
27+
1. **Discovery**: The VerbRegistry in ai-agent scans both the ai-agent verbs package and the dataplane ticketing package using ClassGraph.
28+
29+
2. **Loose Coupling**: The ai-agent doesn't import or depend on dataplane classes directly. Instead, it discovers them at runtime through the Spring ApplicationContext.
30+
31+
3. **Runtime Integration**: When both modules are loaded in the same Spring application, the VerbRegistry can find and use the JIRA verbs from the dataplane module.
32+
33+
4. **Graceful Degradation**: If the dataplane module is not available, the JIRA verbs simply won't be discovered, and the system continues to work without JIRA capabilities.
34+
35+
## JIRA Capabilities Available to AI Agents
36+
37+
When the dataplane module is loaded, AI agents can discover and use these JIRA capabilities:
38+
39+
### Search for Tickets
40+
- **Verb**: `searchForTickets`
41+
- **Description**: Search for JIRA tickets using JQL or simple text
42+
- **Parameters**: `query` (String)
43+
- **Returns**: List of TicketDTO objects
44+
45+
### Assign Ticket
46+
- **Verb**: `assignTicket`
47+
- **Description**: Assign a JIRA ticket to a user
48+
- **Parameters**: `ticketKey` (String), `user` (User)
49+
- **Returns**: Boolean (success/failure)
50+
51+
### Update Ticket
52+
- **Verb**: `updateTicket`
53+
- **Description**: Add a comment to a JIRA ticket
54+
- **Parameters**: `ticketKey` (String), `user` (User), `message` (String)
55+
- **Returns**: Boolean (success/failure)
56+
57+
### Check JIRA Availability
58+
- **Verb**: `isJiraAvailable`
59+
- **Description**: Check if JIRA integration is configured
60+
- **Parameters**: None
61+
- **Returns**: Boolean (available/unavailable)
62+
63+
## Benefits of This Architecture
64+
65+
1. **Separation of Concerns**: AI Agent focuses on AI functionality, dataplane handles data access
66+
2. **No Direct Dependencies**: Clean module boundaries without circular dependencies
67+
3. **Flexible Deployment**: Modules can be deployed independently
68+
4. **Discoverable Capabilities**: AI agents can dynamically discover available capabilities
69+
5. **Graceful Degradation**: System works even if JIRA integration is not available
70+
71+
## Example Usage
72+
73+
```java
74+
// AI Agent discovers JIRA capabilities
75+
VerbRegistry verbRegistry = applicationContext.getBean(VerbRegistry.class);
76+
verbRegistry.scanClasspath();
77+
78+
// Check if JIRA is available
79+
boolean jiraAvailable = verbRegistry.execute(execution, null, "isJiraAvailable", Map.of());
80+
81+
if (jiraAvailable) {
82+
// Search for tickets
83+
List<TicketDTO> tickets = verbRegistry.execute(execution, null, "searchForTickets",
84+
Map.of("query", "project = SUPPORT AND status = Open"));
85+
86+
// Assign a ticket
87+
boolean assigned = verbRegistry.execute(execution, null, "assignTicket",
88+
Map.of("ticketKey", "SUPPORT-123", "user", currentUser));
89+
}
90+
```
91+
92+
This architecture enables flexible JIRA integration while maintaining clean module boundaries and avoiding circular dependencies.

ai-agent/src/main/java/io/sentrius/agent/analysis/agents/agents/AgentVerb.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@
33
import java.lang.reflect.Method;
44
import java.util.List;
55
import io.sentrius.sso.core.dto.capabilities.ParameterDescriptor;
6-
import io.sentrius.sso.core.model.verbs.DefaultInterpreter;
7-
import io.sentrius.sso.core.model.verbs.InputInterpreterIfc;
8-
import io.sentrius.sso.core.model.verbs.OutputInterpreterIfc;
96
import lombok.Builder;
107
import lombok.Getter;
118

@@ -21,9 +18,11 @@ public class AgentVerb {
2118
private boolean requiresTokenManagement = false;
2219
@Builder.Default
2320
private Class<?> returnType = String.class;
21+
2422
@Builder.Default
25-
Class<? extends OutputInterpreterIfc> outputInterpreter = DefaultInterpreter.class;
26-
Class<? extends InputInterpreterIfc> inputInterpreter = DefaultInterpreter.class;
23+
private String returnName = "";
2724

2825
private String exampleJson = "";
26+
@Builder.Default
27+
private String argName = "arg1";
2928
}

ai-agent/src/main/java/io/sentrius/agent/analysis/agents/agents/ChatAgent.java

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import io.sentrius.agent.analysis.api.UserCommunicationService;
1010
import io.sentrius.agent.config.AgentConfigOptions;
1111
import io.sentrius.sso.core.dto.UserDTO;
12-
import io.sentrius.sso.core.dto.ztat.AgentExecution;
12+
import io.sentrius.sso.core.dto.agents.AgentExecution;
1313
import io.sentrius.sso.core.dto.ztat.ZtatRequestDTO;
1414
import io.sentrius.sso.core.exceptions.ZtatException;
1515
import io.sentrius.sso.core.model.security.Ztat;
@@ -85,7 +85,8 @@ public void onApplicationEvent(final ApplicationReadyEvent event) {
8585
try {
8686
var agentName = agentConfigOptions.getNamePrefix() + "-" + UUID.randomUUID().toString();
8787
var base64PublicKey = agentKeyService.getBase64PublicKey(keyPair.getPublic());
88-
var agentRegistrationDTO = agentClientService.bootstrap(agentName, base64PublicKey
88+
var agentRegistrationDTO = agentClientService.bootstrap(agentConfigOptions.getClientId(), agentName,
89+
base64PublicKey
8990
, keyPair.getPublic().getAlgorithm());
9091

9192
var encryptedSecret = agentRegistrationDTO.getClientSecret();
@@ -143,15 +144,24 @@ public void onApplicationEvent(final ApplicationReadyEvent event) {
143144
throw new RuntimeException(e);
144145
}
145146

147+
int allowedFailures = 20;
148+
log.info("Agent Registered...");
146149
while(running) {
147150

148-
log.info("Agent Registered...");
151+
149152
try {
150153

151154
Thread.sleep(5_000);
152155
agentClientService.heartbeat(agentExecution, agentExecution.getUser().getUsername());
153-
} catch (InterruptedException | ZtatException ex) {
154-
throw new RuntimeException(ex);
156+
allowedFailures = 20; // Reset allowed failures on successful heartbeat
157+
} catch (ZtatException | Exception ex) {
158+
if (allowedFailures-- <= 0) {
159+
log.error("Failed to heartbeat agent after multiple attempts, shutting down...");
160+
throw new RuntimeException(ex);
161+
} else {
162+
log.warn("Heartbeat failed, retrying... Remaining attempts: {}", allowedFailures);
163+
}
164+
155165
}
156166

157167
}

ai-agent/src/main/java/io/sentrius/agent/analysis/agents/agents/PromptBuilder.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,11 @@ public String buildPrompt(boolean applyInstructions)
8181
// your DTO
8282
try {
8383
if (verb.getExampleJson() != null && !verb.getExampleJson().isEmpty()) {
84-
prompt.append(" Example arg1: ").append(verb.getExampleJson()).append("\n");
84+
prompt.append(" Example \"" + verb.getArgName() + "\": ").append(verb.getExampleJson()).append("\n");
8585
} else if (example != null) {
8686
// Serialize the example object to JSON
8787
String exampleJson = JsonUtil.MAPPER.writeValueAsString(example);
88-
prompt.append(" Example arg1: ").append(exampleJson).append("\n");
88+
prompt.append(" Example " + verb.getArgName() + ": ").append(exampleJson).append("\n");
8989
}
9090

9191
} catch (Exception e) {

ai-agent/src/main/java/io/sentrius/agent/analysis/agents/agents/RegisteredAgent.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
import io.sentrius.agent.analysis.agents.verbs.AgentVerbs;
88
import io.sentrius.agent.analysis.api.AgentKeyService;
99
import io.sentrius.agent.config.AgentConfigOptions;
10-
import io.sentrius.sso.core.dto.ztat.AgentExecution;
10+
import io.sentrius.sso.core.dto.agents.AgentExecution;
11+
import io.sentrius.sso.core.dto.agents.AgentExecutionContextDTO;
1112
import io.sentrius.sso.core.dto.ztat.ZtatRequestDTO;
1213
import io.sentrius.sso.core.exceptions.ZtatException;
1314
import io.sentrius.sso.core.model.security.Ztat;
@@ -120,6 +121,7 @@ public void onApplicationEvent(final ApplicationReadyEvent event) {
120121

121122
var agentExecution = agentExecutionService.getAgentExecution(finalUser);
122123
var response = promptAgent(agentExecution);
124+
AgentExecutionContextDTO agentExecutionContext = AgentExecutionContextDTO.builder().build();
123125
while (running) {
124126
try {
125127
log.info("Got response: {}", response);
@@ -131,7 +133,8 @@ public void onApplicationEvent(final ApplicationReadyEvent event) {
131133
if (node.get("verb") != null) {
132134
var verb = node.get("verb").asText();
133135
log.info("Executing verb: {}", verb);
134-
priorResponse = verbRegistry.execute(agentExecution, priorResponse, verb, args);
136+
priorResponse = verbRegistry.execute(agentExecution,agentExecutionContext,
137+
priorResponse, verb, args);
135138
}
136139
log.info("Node: {}", node);
137140
}

0 commit comments

Comments
 (0)