Skip to content

Commit 8dc6f2e

Browse files
authored
feat: Add JPA Database TaskStore (#293)
# Description Fixes #279 and #278
1 parent 3be6ee9 commit 8dc6f2e

File tree

14 files changed

+832
-1
lines changed

14 files changed

+832
-1
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -638,5 +638,6 @@ To contribute an integration, please see [CONTRIBUTING_INTEGRATIONS.md](CONTRIBU
638638
* [reference/grpc/README.md](reference/grpc/README.md) - gRPC Reference implementation, based on Quarkus.
639639
* https://github.com/wildfly-extras/a2a-java-sdk-server-jakarta - This integration is based on Jakarta EE, and should work in all runtimes supporting the [Jakarta EE Web Profile](https://jakarta.ee/specifications/webprofile/).
640640

641-
641+
# Extras
642+
See the [`extras`](./extras/README.md) folder for extra functionality not provided by the SDK itself!
642643

extras/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# A2A Java SDK - Extras
2+
3+
This directory contains additions to what is provided by the default SDK implementations.
4+
5+
Please see the README's of each child directory for more details.
6+
7+
[`taskstore-database-jpa`](./taskstore-database-jpa/README.md) - Replaces the default `InMemoryTaskStore` with a `TaskStore` backed by a RDBMS. It uses JPA to interact with the RDBMS.
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# A2A Java SDK - JPA Database TaskStore
2+
3+
This module provides a JPA-based implementation of the `TaskStore` interface that persists tasks to a relational database instead of keeping them in memory.
4+
5+
The persistence is done with the Jakarta Persistence API, so this should be suitable for any JPA 3.0+ provider and Jakarta EE application server.
6+
7+
## Quick Start
8+
9+
### 1. Add Dependency
10+
11+
Add this module to your project's `pom.xml`:
12+
13+
```xml
14+
<dependency>
15+
<groupId>io.github.a2asdk</groupId>
16+
<artifactId>a2a-java-extras-taskstore-database-jpa</artifactId>
17+
<version>${a2a.version}</version>
18+
</dependency>
19+
```
20+
21+
The `JpaDatabaseTaskStore` is annotated in such a way that it should take precedence over the default `InMemoryTaskStore`. Hence, it is a drop-in replacement.
22+
23+
### 2. Configure Database
24+
25+
The following examples assume you are using PostgreSQL as your database. To use another database, adjust as needed for your environment.
26+
27+
#### For Quarkus Reference Servers
28+
29+
Add to your `application.properties`:
30+
31+
```properties
32+
# Database configuration
33+
quarkus.datasource.db-kind=postgresql
34+
quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/a2a_tasks
35+
quarkus.datasource.username=your_username
36+
quarkus.datasource.password=your_password
37+
38+
# Hibernate configuration
39+
quarkus.hibernate-orm.database.generation=update
40+
```
41+
42+
#### For WildFly/Jakarta EE Servers
43+
44+
Create or update your `persistence.xml`:
45+
46+
```xml
47+
<?xml version="1.0" encoding="UTF-8"?>
48+
<persistence xmlns="https://jakarta.ee/xml/ns/persistence" version="3.0">
49+
<persistence-unit name="a2a-java" transaction-type="JTA">
50+
<jta-data-source>java:jboss/datasources/A2ATasksDS</jta-data-source>
51+
52+
<class>io.a2a.extras.taskstore.database.jpa.JpaTask</class>
53+
<exclude-unlisted-classes>true</exclude-unlisted-classes>
54+
55+
<properties>
56+
<!-- Change as required for your environment -->
57+
<property name="jakarta.persistence.schema-generation.database.action" value="create"/>
58+
<property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect"/>
59+
</properties>
60+
</persistence-unit>
61+
</persistence>
62+
```
63+
64+
### 3. Database Schema
65+
66+
The module will automatically create the required table:
67+
68+
```sql
69+
CREATE TABLE a2a_tasks (
70+
task_id VARCHAR(255) PRIMARY KEY,
71+
task_data TEXT NOT NULL
72+
);
73+
```
74+
75+
## Configuration Options
76+
77+
### Persistence Unit Name
78+
79+
The module uses the persistence unit name `"a2a-java"`. Ensure your `persistence.xml` defines a persistence unit with this name.
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<?xml version="1.0"?>
2+
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"
3+
xmlns="http://maven.apache.org/POM/4.0.0"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<parent>
8+
<groupId>io.github.a2asdk</groupId>
9+
<artifactId>a2a-java-sdk-parent</artifactId>
10+
<version>0.3.0.Beta2-SNAPSHOT</version>
11+
<relativePath>../../pom.xml</relativePath>
12+
</parent>
13+
<artifactId>a2a-java-extras-taskstore-database-jpa</artifactId>
14+
15+
<packaging>jar</packaging>
16+
17+
<name>Java A2A Extras: JPA Database TaskStore</name>
18+
<description>Java SDK for the Agent2Agent Protocol (A2A) - Extras - JPA Database TaskStore</description>
19+
20+
<dependencies>
21+
<dependency>
22+
<groupId>${project.groupId}</groupId>
23+
<artifactId>a2a-java-sdk-server-common</artifactId>
24+
</dependency>
25+
<dependency>
26+
<groupId>jakarta.annotation</groupId>
27+
<artifactId>jakarta.annotation-api</artifactId>
28+
</dependency>
29+
<dependency>
30+
<groupId>jakarta.enterprise</groupId>
31+
<artifactId>jakarta.enterprise.cdi-api</artifactId>
32+
</dependency>
33+
<dependency>
34+
<groupId>jakarta.inject</groupId>
35+
<artifactId>jakarta.inject-api</artifactId>
36+
</dependency>
37+
<dependency>
38+
<groupId>jakarta.persistence</groupId>
39+
<artifactId>jakarta.persistence-api</artifactId>
40+
</dependency>
41+
<dependency>
42+
<groupId>org.slf4j</groupId>
43+
<artifactId>slf4j-api</artifactId>
44+
</dependency>
45+
46+
<dependency>
47+
<groupId>io.quarkus</groupId>
48+
<artifactId>quarkus-junit5</artifactId>
49+
<scope>test</scope>
50+
</dependency>
51+
<dependency>
52+
<groupId>io.quarkus</groupId>
53+
<artifactId>quarkus-rest-client-jackson</artifactId>
54+
<scope>test</scope>
55+
</dependency>
56+
<dependency>
57+
<groupId>org.junit.jupiter</groupId>
58+
<artifactId>junit-jupiter-api</artifactId>
59+
<scope>test</scope>
60+
</dependency>
61+
<dependency>
62+
<groupId>io.rest-assured</groupId>
63+
<artifactId>rest-assured</artifactId>
64+
<scope>test</scope>
65+
</dependency>
66+
<dependency>
67+
<groupId>jakarta.transaction</groupId>
68+
<artifactId>jakarta.transaction-api</artifactId>
69+
</dependency>
70+
<dependency>
71+
<groupId>io.quarkus</groupId>
72+
<artifactId>quarkus-hibernate-orm</artifactId>
73+
<scope>test</scope>
74+
</dependency>
75+
<dependency>
76+
<groupId>io.quarkus</groupId>
77+
<artifactId>quarkus-jdbc-h2</artifactId>
78+
<scope>test</scope>
79+
</dependency>
80+
<!-- Additional dependencies for integration tests (from reference/jsonrpc) -->
81+
<dependency>
82+
<groupId>${project.groupId}</groupId>
83+
<artifactId>a2a-java-sdk-reference-jsonrpc</artifactId>
84+
<scope>test</scope>
85+
</dependency>
86+
<dependency>
87+
<groupId>${project.groupId}</groupId>
88+
<artifactId>a2a-java-sdk-client-transport-jsonrpc</artifactId>
89+
<scope>test</scope>
90+
</dependency>
91+
<dependency>
92+
<groupId>${project.groupId}</groupId>
93+
<artifactId>a2a-java-sdk-client</artifactId>
94+
<scope>test</scope>
95+
</dependency>
96+
<dependency>
97+
<groupId>io.quarkus</groupId>
98+
<artifactId>quarkus-reactive-routes</artifactId>
99+
<scope>test</scope>
100+
</dependency>
101+
</dependencies>
102+
</project>
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package io.a2a.extras.taskstore.database.jpa;
2+
3+
import jakarta.annotation.Priority;
4+
import jakarta.enterprise.context.ApplicationScoped;
5+
import jakarta.enterprise.inject.Alternative;
6+
import jakarta.persistence.EntityManager;
7+
import jakarta.persistence.PersistenceContext;
8+
import jakarta.transaction.Transactional;
9+
10+
import com.fasterxml.jackson.core.JsonProcessingException;
11+
import io.a2a.server.tasks.TaskStore;
12+
import io.a2a.spec.Task;
13+
import org.slf4j.Logger;
14+
import org.slf4j.LoggerFactory;
15+
16+
@ApplicationScoped
17+
@Alternative
18+
@Priority(50)
19+
public class JpaDatabaseTaskStore implements TaskStore {
20+
21+
private static final Logger LOGGER = LoggerFactory.getLogger(JpaDatabaseTaskStore.class);
22+
23+
@PersistenceContext(unitName = "a2a-java")
24+
EntityManager em;
25+
26+
@Transactional
27+
@Override
28+
public void save(Task task) {
29+
LOGGER.debug("Saving task with ID: {}", task.getId());
30+
try {
31+
JpaTask jpaTask = JpaTask.createFromTask(task);
32+
em.merge(jpaTask);
33+
LOGGER.debug("Persisted/updated task with ID: {}", task.getId());
34+
} catch (JsonProcessingException e) {
35+
LOGGER.error("Failed to serialize task with ID: {}", task.getId(), e);
36+
throw new RuntimeException("Failed to serialize task with ID: " + task.getId(), e);
37+
}
38+
}
39+
40+
@Transactional
41+
@Override
42+
public Task get(String taskId) {
43+
LOGGER.debug("Retrieving task with ID: {}", taskId);
44+
JpaTask jpaTask = em.find(JpaTask.class, taskId);
45+
if (jpaTask == null) {
46+
LOGGER.debug("Task not found with ID: {}", taskId);
47+
return null;
48+
}
49+
50+
try {
51+
Task task = jpaTask.getTask();
52+
LOGGER.debug("Successfully retrieved task with ID: {}", taskId);
53+
return task;
54+
} catch (JsonProcessingException e) {
55+
LOGGER.error("Failed to deserialize task with ID: {}", taskId, e);
56+
throw new RuntimeException("Failed to deserialize task with ID: " + taskId, e);
57+
}
58+
}
59+
60+
@Transactional
61+
@Override
62+
public void delete(String taskId) {
63+
LOGGER.debug("Deleting task with ID: {}", taskId);
64+
JpaTask jpaTask = em.find(JpaTask.class, taskId);
65+
if (jpaTask != null) {
66+
em.remove(jpaTask);
67+
LOGGER.debug("Successfully deleted task with ID: {}", taskId);
68+
} else {
69+
LOGGER.debug("Task not found for deletion with ID: {}", taskId);
70+
}
71+
}
72+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package io.a2a.extras.taskstore.database.jpa;
2+
3+
import jakarta.persistence.Column;
4+
import jakarta.persistence.Entity;
5+
import jakarta.persistence.Id;
6+
import jakarta.persistence.Table;
7+
import jakarta.persistence.Transient;
8+
9+
import com.fasterxml.jackson.core.JsonProcessingException;
10+
import io.a2a.spec.Task;
11+
import io.a2a.util.Utils;
12+
13+
@Entity
14+
@Table(name = "a2a_tasks")
15+
public class JpaTask {
16+
@Id
17+
@Column(name = "task_id")
18+
private String id;
19+
20+
@Column(name = "task_data", columnDefinition = "TEXT", nullable = false)
21+
private String taskJson;
22+
23+
@Transient
24+
private Task task;
25+
26+
// Default constructor required by JPA
27+
public JpaTask() {
28+
}
29+
30+
public JpaTask(String id, String taskJson) {
31+
this.id = id;
32+
this.taskJson = taskJson;
33+
}
34+
35+
public String getId() {
36+
return id;
37+
}
38+
39+
public void setId(String id) {
40+
this.id = id;
41+
}
42+
43+
public String getTaskJson() {
44+
return taskJson;
45+
}
46+
47+
public void setTaskJson(String taskJson) {
48+
this.taskJson = taskJson;
49+
}
50+
51+
public Task getTask() throws JsonProcessingException {
52+
if (task == null) {
53+
this.task = Utils.unmarshalFrom(taskJson, Task.TYPE_REFERENCE);
54+
}
55+
return task;
56+
}
57+
58+
public void setTask(Task task) throws JsonProcessingException {
59+
taskJson = Utils.OBJECT_MAPPER.writeValueAsString(task);
60+
if (id == null) {
61+
id = task.getId();
62+
}
63+
this.task = task;
64+
}
65+
66+
static JpaTask createFromTask(Task task) throws JsonProcessingException {
67+
String json = Utils.OBJECT_MAPPER.writeValueAsString(task);
68+
JpaTask jpaTask = new JpaTask(task.getId(), json);
69+
jpaTask.task = task;
70+
return jpaTask;
71+
}
72+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<beans xmlns="https://jakarta.ee/xml/ns/jakartaee"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
5+
https://jakarta.ee/xml/ns/jakartaee/beans_4_0.xsd">
6+
</beans>

0 commit comments

Comments
 (0)