Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions .local.env
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
SENTRIUS_VERSION=1.1.261
SENTRIUS_SSH_VERSION=1.1.40
SENTRIUS_KEYCLOAK_VERSION=1.1.52
SENTRIUS_AGENT_VERSION=1.1.39
SENTRIUS_AI_AGENT_VERSION=1.1.148
LLMPROXY_VERSION=1.0.53
LAUNCHER_VERSION=1.0.73
AGENTPROXY_VERSION=1.0.74
SENTRIUS_VERSION=1.1.325
SENTRIUS_SSH_VERSION=1.1.41
SENTRIUS_KEYCLOAK_VERSION=1.1.53
SENTRIUS_AGENT_VERSION=1.1.42
SENTRIUS_AI_AGENT_VERSION=1.1.263
LLMPROXY_VERSION=1.0.78
LAUNCHER_VERSION=1.0.82
AGENTPROXY_VERSION=1.0.75
16 changes: 8 additions & 8 deletions .local.env.bak
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
SENTRIUS_VERSION=1.1.261
SENTRIUS_SSH_VERSION=1.1.40
SENTRIUS_KEYCLOAK_VERSION=1.1.52
SENTRIUS_AGENT_VERSION=1.1.39
SENTRIUS_AI_AGENT_VERSION=1.1.148
LLMPROXY_VERSION=1.0.53
LAUNCHER_VERSION=1.0.73
AGENTPROXY_VERSION=1.0.74
SENTRIUS_VERSION=1.1.325
SENTRIUS_SSH_VERSION=1.1.41
SENTRIUS_KEYCLOAK_VERSION=1.1.53
SENTRIUS_AGENT_VERSION=1.1.42
SENTRIUS_AI_AGENT_VERSION=1.1.263
LLMPROXY_VERSION=1.0.78
LAUNCHER_VERSION=1.0.82
AGENTPROXY_VERSION=1.0.75
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,19 @@
@Slf4j
@RestController
@RequestMapping(ApiPaths.API_V1 + "/agent/launcher")
public class AgentLauncherController {
public class AgentLauncherController {
private final PodLauncherService podLauncherService;
private final KeycloakService keycloakService;

public AgentLauncherController(
PodLauncherService podLauncherService, KeycloakService keycloakService) {
protected AgentLauncherController(
PodLauncherService podLauncherService, KeycloakService keycloakService
) {

this.podLauncherService = podLauncherService;
this.keycloakService = keycloakService;
}


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

// create the user, assign the policy


podLauncherService.launchAgentPod(agent);

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

@GetMapping("/status")
public ResponseEntity<String> getAgentStatus(@RequestParam(name="agentId") String agentId) {
public ResponseEntity<?> getAgentStatus(@RequestParam(name="agentId") String agentId) {
try {
return ResponseEntity.ok(podLauncherService.statusById(agentId) );
return ResponseEntity.ok(Map.of("status", podLauncherService.statusById(agentId)) );
} catch (Exception e) {
log.error("Status failed", e);
return ResponseEntity.status(500).body("Status retrieval failed");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,12 @@ public V1Pod launchAgentPod(AgentRegistrationDTO agent) throws Exception {
List<String> argList = new ArrayList<>();
argList.add("--spring.config.location=file:/config/agent.properties");
argList.add("--agent.namePrefix=" + agentId);
argList.add("--agent.clientId=" + agent.getClientId());
argList.add("--agent.listen.websocket=true");
argList.add("--agent.callback.url=" + constructedCallbackUrl);
if (agent.getAgentPolicyId() != null && !agent.getAgentPolicyId().isEmpty()) {
argList.add("--agent.ai.policy.id=" + agent.getAgentPolicyId());
}
if (agent.getAgentContextId() != null && !agent.getAgentContextId().isEmpty()) {
argList.add("--agent.ai.context.db.id=" + agent.getAgentContextId());
}else {
Expand Down Expand Up @@ -215,8 +219,8 @@ public V1Pod launchAgentPod(AgentRegistrationDTO agent) throws Exception {
.args(argList)
.resources(new V1ResourceRequirements()
.limits(Map.of(
"cpu", Quantity.fromString("1000m"),
"memory", Quantity.fromString("1Gi")
"cpu", Quantity.fromString("2000m"),
"memory", Quantity.fromString("2Gi")
)))
.volumeMounts(List.of(
new V1VolumeMount()
Expand Down
92 changes: 92 additions & 0 deletions ai-agent/README-JIRA-INTEGRATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# JIRA Integration Architecture

## Overview

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.

## Architecture

```
ai-agent module (no direct dependency on dataplane)
├── VerbRegistry - discovers verbs from both modules
│ ├── Scans: "io.sentrius.agent.analysis.agents.verbs"
│ └── Scans: "io.sentrius.sso.core.integrations.ticketing"
└── Gets JIRA verbs from Spring ApplicationContext

dataplane module (contains JIRA implementation)
├── JiraVerbService - provides JIRA operations as @Verb methods
│ ├── searchForTickets(@Verb)
│ ├── assignTicket(@Verb)
│ ├── updateTicket(@Verb)
│ └── isJiraAvailable(@Verb)
└── JiraService - actual JIRA integration logic
```

## How It Works

1. **Discovery**: The VerbRegistry in ai-agent scans both the ai-agent verbs package and the dataplane ticketing package using ClassGraph.

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.

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.

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.

## JIRA Capabilities Available to AI Agents

When the dataplane module is loaded, AI agents can discover and use these JIRA capabilities:

### Search for Tickets
- **Verb**: `searchForTickets`
- **Description**: Search for JIRA tickets using JQL or simple text
- **Parameters**: `query` (String)
- **Returns**: List of TicketDTO objects

### Assign Ticket
- **Verb**: `assignTicket`
- **Description**: Assign a JIRA ticket to a user
- **Parameters**: `ticketKey` (String), `user` (User)
- **Returns**: Boolean (success/failure)

### Update Ticket
- **Verb**: `updateTicket`
- **Description**: Add a comment to a JIRA ticket
- **Parameters**: `ticketKey` (String), `user` (User), `message` (String)
- **Returns**: Boolean (success/failure)

### Check JIRA Availability
- **Verb**: `isJiraAvailable`
- **Description**: Check if JIRA integration is configured
- **Parameters**: None
- **Returns**: Boolean (available/unavailable)

## Benefits of This Architecture

1. **Separation of Concerns**: AI Agent focuses on AI functionality, dataplane handles data access
2. **No Direct Dependencies**: Clean module boundaries without circular dependencies
3. **Flexible Deployment**: Modules can be deployed independently
4. **Discoverable Capabilities**: AI agents can dynamically discover available capabilities
5. **Graceful Degradation**: System works even if JIRA integration is not available

## Example Usage

```java
// AI Agent discovers JIRA capabilities
VerbRegistry verbRegistry = applicationContext.getBean(VerbRegistry.class);
verbRegistry.scanClasspath();

// Check if JIRA is available
boolean jiraAvailable = verbRegistry.execute(execution, null, "isJiraAvailable", Map.of());

if (jiraAvailable) {
// Search for tickets
List<TicketDTO> tickets = verbRegistry.execute(execution, null, "searchForTickets",
Map.of("query", "project = SUPPORT AND status = Open"));

// Assign a ticket
boolean assigned = verbRegistry.execute(execution, null, "assignTicket",
Map.of("ticketKey", "SUPPORT-123", "user", currentUser));
}
```

This architecture enables flexible JIRA integration while maintaining clean module boundaries and avoiding circular dependencies.
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@
import java.lang.reflect.Method;
import java.util.List;
import io.sentrius.sso.core.dto.capabilities.ParameterDescriptor;
import io.sentrius.sso.core.model.verbs.DefaultInterpreter;
import io.sentrius.sso.core.model.verbs.InputInterpreterIfc;
import io.sentrius.sso.core.model.verbs.OutputInterpreterIfc;
import lombok.Builder;
import lombok.Getter;

Expand All @@ -21,9 +18,11 @@ public class AgentVerb {
private boolean requiresTokenManagement = false;
@Builder.Default
private Class<?> returnType = String.class;

@Builder.Default
Class<? extends OutputInterpreterIfc> outputInterpreter = DefaultInterpreter.class;
Class<? extends InputInterpreterIfc> inputInterpreter = DefaultInterpreter.class;
private String returnName = "";

private String exampleJson = "";
@Builder.Default
private String argName = "arg1";
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import io.sentrius.agent.analysis.api.UserCommunicationService;
import io.sentrius.agent.config.AgentConfigOptions;
import io.sentrius.sso.core.dto.UserDTO;
import io.sentrius.sso.core.dto.ztat.AgentExecution;
import io.sentrius.sso.core.dto.agents.AgentExecution;
import io.sentrius.sso.core.dto.ztat.ZtatRequestDTO;
import io.sentrius.sso.core.exceptions.ZtatException;
import io.sentrius.sso.core.model.security.Ztat;
Expand Down Expand Up @@ -85,7 +85,8 @@ public void onApplicationEvent(final ApplicationReadyEvent event) {
try {
var agentName = agentConfigOptions.getNamePrefix() + "-" + UUID.randomUUID().toString();
var base64PublicKey = agentKeyService.getBase64PublicKey(keyPair.getPublic());
var agentRegistrationDTO = agentClientService.bootstrap(agentName, base64PublicKey
var agentRegistrationDTO = agentClientService.bootstrap(agentConfigOptions.getClientId(), agentName,
base64PublicKey
, keyPair.getPublic().getAlgorithm());

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

int allowedFailures = 20;
log.info("Agent Registered...");
while(running) {

log.info("Agent Registered...");

try {

Thread.sleep(5_000);
agentClientService.heartbeat(agentExecution, agentExecution.getUser().getUsername());
} catch (InterruptedException | ZtatException ex) {
throw new RuntimeException(ex);
allowedFailures = 20; // Reset allowed failures on successful heartbeat
} catch (ZtatException | Exception ex) {
if (allowedFailures-- <= 0) {
log.error("Failed to heartbeat agent after multiple attempts, shutting down...");
throw new RuntimeException(ex);
} else {
log.warn("Heartbeat failed, retrying... Remaining attempts: {}", allowedFailures);
}

}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,11 @@ public String buildPrompt(boolean applyInstructions)
// your DTO
try {
if (verb.getExampleJson() != null && !verb.getExampleJson().isEmpty()) {
prompt.append(" Example arg1: ").append(verb.getExampleJson()).append("\n");
prompt.append(" Example \"" + verb.getArgName() + "\": ").append(verb.getExampleJson()).append("\n");
} else if (example != null) {
// Serialize the example object to JSON
String exampleJson = JsonUtil.MAPPER.writeValueAsString(example);
prompt.append(" Example arg1: ").append(exampleJson).append("\n");
prompt.append(" Example " + verb.getArgName() + ": ").append(exampleJson).append("\n");
}

} catch (Exception e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
import io.sentrius.agent.analysis.agents.verbs.AgentVerbs;
import io.sentrius.agent.analysis.api.AgentKeyService;
import io.sentrius.agent.config.AgentConfigOptions;
import io.sentrius.sso.core.dto.ztat.AgentExecution;
import io.sentrius.sso.core.dto.agents.AgentExecution;
import io.sentrius.sso.core.dto.agents.AgentExecutionContextDTO;
import io.sentrius.sso.core.dto.ztat.ZtatRequestDTO;
import io.sentrius.sso.core.exceptions.ZtatException;
import io.sentrius.sso.core.model.security.Ztat;
Expand Down Expand Up @@ -120,6 +121,7 @@ public void onApplicationEvent(final ApplicationReadyEvent event) {

var agentExecution = agentExecutionService.getAgentExecution(finalUser);
var response = promptAgent(agentExecution);
AgentExecutionContextDTO agentExecutionContext = AgentExecutionContextDTO.builder().build();
while (running) {
try {
log.info("Got response: {}", response);
Expand All @@ -131,7 +133,8 @@ public void onApplicationEvent(final ApplicationReadyEvent event) {
if (node.get("verb") != null) {
var verb = node.get("verb").asText();
log.info("Executing verb: {}", verb);
priorResponse = verbRegistry.execute(agentExecution, priorResponse, verb, args);
priorResponse = verbRegistry.execute(agentExecution,agentExecutionContext,
priorResponse, verb, args);
}
log.info("Node: {}", node);
}
Expand Down
Loading