Skip to content

Commit d2d4a93

Browse files
committed
Use graph object manager for user ops
1 parent acb0dbd commit d2d4a93

12 files changed

Lines changed: 777 additions & 14 deletions

File tree

pom.xml

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
<properties>
2020
<java.version>21</java.version>
2121
<embabel-agent.version>0.3.1</embabel-agent.version>
22-
<kotlin.version>2.0.20</kotlin.version>
22+
<kotlin.version>2.2.0</kotlin.version>
2323
</properties>
2424

2525
<dependencies>
@@ -35,12 +35,18 @@
3535
<groupId>com.embabel.agent</groupId>
3636
<artifactId>embabel-agent-rag-neo-drivine</artifactId>
3737
<version>0.1.1-SNAPSHOT</version>
38+
<exclusions>
39+
<exclusion>
40+
<groupId>org.drivine</groupId>
41+
<artifactId>drivine4j</artifactId>
42+
</exclusion>
43+
</exclusions>
3844
</dependency>
3945

4046
<dependency>
4147
<groupId>org.drivine</groupId>
4248
<artifactId>drivine4j-spring-boot-starter</artifactId>
43-
<version>0.0.8</version>
49+
<version>0.0.12</version>
4450
</dependency>
4551

4652

@@ -168,6 +174,52 @@
168174
<groupId>org.springframework.boot</groupId>
169175
<artifactId>spring-boot-maven-plugin</artifactId>
170176
</plugin>
177+
178+
<!-- Run Gradle KSP to generate Drivine DSL code -->
179+
<plugin>
180+
<groupId>org.codehaus.mojo</groupId>
181+
<artifactId>exec-maven-plugin</artifactId>
182+
<version>3.5.0</version>
183+
<executions>
184+
<execution>
185+
<id>gradle-ksp-codegen</id>
186+
<phase>generate-sources</phase>
187+
<goals>
188+
<goal>exec</goal>
189+
</goals>
190+
<configuration>
191+
<executable>${project.basedir}/codegen-gradle/gradlew</executable>
192+
<workingDirectory>${project.basedir}/codegen-gradle</workingDirectory>
193+
<arguments>
194+
<argument>kspKotlin</argument>
195+
<argument>--quiet</argument>
196+
</arguments>
197+
</configuration>
198+
</execution>
199+
</executions>
200+
</plugin>
201+
202+
<!-- Include Gradle-generated sources in Maven build -->
203+
<plugin>
204+
<groupId>org.codehaus.mojo</groupId>
205+
<artifactId>build-helper-maven-plugin</artifactId>
206+
<version>3.6.0</version>
207+
<executions>
208+
<execution>
209+
<id>add-ksp-generated-sources</id>
210+
<phase>generate-sources</phase>
211+
<goals>
212+
<goal>add-source</goal>
213+
</goals>
214+
<configuration>
215+
<sources>
216+
<source>${project.basedir}/codegen-gradle/build/generated/ksp/main/kotlin</source>
217+
</sources>
218+
</configuration>
219+
</execution>
220+
</executions>
221+
</plugin>
222+
171223
<plugin>
172224
<groupId>org.jetbrains.kotlin</groupId>
173225
<artifactId>kotlin-maven-plugin</artifactId>
@@ -183,6 +235,7 @@
183235
<sourceDirs>
184236
<sourceDir>${project.basedir}/src/main/kotlin</sourceDir>
185237
<sourceDir>${project.basedir}/src/main/java</sourceDir>
238+
<sourceDir>${project.basedir}/codegen-gradle/build/generated/ksp/main/kotlin</sourceDir>
186239
</sourceDirs>
187240
</configuration>
188241
</execution>
@@ -204,6 +257,7 @@
204257
<args>
205258
<arg>-Xjsr305=strict</arg>
206259
<arg>-Xskip-metadata-version-check</arg>
260+
<arg>-Xcontext-parameters</arg>
207261
</args>
208262
<compilerPlugins>
209263
<plugin>spring</plugin>

src/main/java/com/embabel/GuideApplication.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717

1818
import org.drivine.autoconfigure.EnableDrivine;
1919
import org.drivine.autoconfigure.EnableDrivinePropertiesConfig;
20+
import org.drivine.manager.GraphObjectManager;
21+
import org.drivine.manager.GraphObjectManagerFactory;
2022
import org.drivine.manager.PersistenceManager;
2123
import org.drivine.manager.PersistenceManagerFactory;
2224
import org.springframework.boot.SpringApplication;
@@ -44,4 +46,9 @@ public static void main(String[] args) {
4446
public PersistenceManager neoManager(PersistenceManagerFactory factory) {
4547
return factory.get("neo");
4648
}
49+
50+
@Bean("neoGraphObjectManager")
51+
public GraphObjectManager neoGraphObjectManager(GraphObjectManagerFactory factory) {
52+
return factory.get("neo");
53+
}
4754
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.embabel.guide.domain
2+
3+
import org.drivine.annotation.Direction
4+
import org.drivine.annotation.GraphRelationship
5+
import org.drivine.annotation.GraphView
6+
import org.drivine.annotation.Root
7+
8+
/**
9+
* GraphView specifically for GuideUsers with anonymous WebUser relationships.
10+
* The AnonymousWebUserData has labels ["WebUser", "Anonymous"] which ensures
11+
* only anonymous users are matched.
12+
*/
13+
@GraphView
14+
data class AnonymousGuideUser(
15+
@Root
16+
val core: GuideUserData,
17+
18+
@GraphRelationship(type = "IS_WEB_USER", direction = Direction.OUTGOING)
19+
val webUser: AnonymousWebUserData
20+
) {
21+
/**
22+
* Convert to the unified GuideUser type for consistent API.
23+
*/
24+
fun toGuideUser(): GuideUser = GuideUser(
25+
core = core,
26+
webUser = webUser
27+
)
28+
}
Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
package com.embabel.guide.domain
22

33
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
4+
import org.drivine.annotation.NodeFragment
45

56
/**
6-
* Data representation for anonymous web users.
7-
* Extends WebUserData to add the Anonymous label distinction.
7+
* Node fragment for anonymous web users.
8+
* Has both WebUser and Anonymous labels in the graph.
89
*/
10+
@NodeFragment(labels = ["WebUser", "Anonymous"])
911
@JsonIgnoreProperties(ignoreUnknown = true)
1012
class AnonymousWebUserData(
11-
userId: String,
12-
userDisplayName: String,
13-
userUsername: String,
13+
id: String,
14+
displayName: String,
15+
userName: String,
1416
userEmail: String?,
1517
passwordHash: String?,
1618
refreshToken: String?
17-
) : WebUserData(userId, userDisplayName, userUsername, userEmail, passwordHash, refreshToken) {
19+
) : WebUserData(id, displayName, userName, userEmail, passwordHash, refreshToken) {
1820

1921
override fun toString(): String =
20-
"AnonymousWebUserData{userId='$id', userDisplayName='$displayName', userUsername='$userName'}"
22+
"AnonymousWebUserData{id='$id', displayName='$displayName', userName='$userName'}"
2123
}

src/main/kotlin/com/embabel/guide/domain/DiscordUserInfoData.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@ package com.embabel.guide.domain
22

33
import com.embabel.agent.discord.DiscordUserInfo
44
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
5+
import org.drivine.annotation.NodeFragment
6+
import org.drivine.annotation.NodeId
57

68
/**
7-
* Simple data representation of DiscordUserInfo properties for Drivine composition.
9+
* Node fragment representing Discord user info in the graph.
810
*/
11+
@NodeFragment(labels = ["DiscordUserInfo"])
912
@JsonIgnoreProperties(ignoreUnknown = true)
1013
data class DiscordUserInfoData(
14+
@NodeId
1115
var id: String? = null,
1216
var username: String? = null,
1317
var discriminator: String? = null,
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package com.embabel.guide.domain
2+
3+
import org.drivine.manager.GraphObjectManager
4+
import org.drivine.query.dsl.*
5+
import org.springframework.beans.factory.annotation.Qualifier
6+
import org.springframework.stereotype.Repository
7+
import org.springframework.transaction.annotation.Transactional
8+
import java.util.Optional
9+
10+
/**
11+
* GraphView-based implementation of GuideUserRepository.
12+
* Uses the type-safe DSL generated by Drivine KSP.
13+
*/
14+
@Repository
15+
class GraphObjectGuideUserRepository(
16+
@Qualifier("neoGraphObjectManager") private val graphObjectManager: GraphObjectManager
17+
) : GuideUserRepository {
18+
19+
@Transactional(readOnly = true)
20+
override fun findByDiscordUserId(discordUserId: String): Optional<GuideUser> {
21+
val results = graphObjectManager.loadAll<GuideUser> {
22+
where {
23+
query.discordUserInfo.id eq discordUserId
24+
}
25+
}
26+
return Optional.ofNullable(results.firstOrNull())
27+
}
28+
29+
@Transactional(readOnly = true)
30+
override fun findByWebUserId(webUserId: String): Optional<GuideUser> {
31+
val results = graphObjectManager.loadAll<GuideUser> {
32+
where {
33+
query.webUser.id eq webUserId
34+
}
35+
}
36+
return Optional.ofNullable(results.firstOrNull())
37+
}
38+
39+
@Transactional(readOnly = true)
40+
override fun findAnonymousWebUser(): Optional<GuideUser> {
41+
// Use AnonymousGuideUser GraphView which only matches users with Anonymous label
42+
val results = graphObjectManager.loadAll(AnonymousGuideUser::class.java)
43+
return Optional.ofNullable(results.firstOrNull()?.toGuideUser())
44+
}
45+
46+
@Transactional(readOnly = true)
47+
override fun findByWebUserName(userName: String): Optional<GuideUser> {
48+
val results = graphObjectManager.loadAll<GuideUser> {
49+
where {
50+
query.webUser.userName eq userName
51+
}
52+
}
53+
return Optional.ofNullable(results.firstOrNull())
54+
}
55+
56+
@Transactional(readOnly = true)
57+
override fun findById(id: String): Optional<GuideUser> {
58+
val results = graphObjectManager.loadAll<GuideUser> {
59+
where {
60+
query.core.id eq id
61+
}
62+
}
63+
return Optional.ofNullable(results.firstOrNull())
64+
}
65+
66+
@Transactional
67+
override fun createWithDiscord(
68+
guideUserData: GuideUserData,
69+
discordUserInfo: DiscordUserInfoData
70+
): GuideUser {
71+
val guideUser = GuideUser(
72+
core = guideUserData,
73+
discordUserInfo = discordUserInfo
74+
)
75+
return graphObjectManager.save(guideUser)
76+
}
77+
78+
@Transactional
79+
override fun createWithWebUser(
80+
guideUserData: GuideUserData,
81+
webUserData: WebUserData
82+
): GuideUser {
83+
val guideUser = GuideUser(
84+
core = guideUserData,
85+
webUser = webUserData
86+
)
87+
return graphObjectManager.save(guideUser)
88+
}
89+
90+
@Transactional
91+
override fun save(guideUser: GuideUser): GuideUser {
92+
return graphObjectManager.save(guideUser)
93+
}
94+
95+
@Transactional
96+
override fun updatePersona(guideUserId: String, persona: String) {
97+
val guideUser = findById(guideUserId).orElseThrow {
98+
IllegalArgumentException("GuideUser not found: $guideUserId")
99+
}
100+
val updated = guideUser.copy(core = guideUser.core.copy(persona = persona))
101+
graphObjectManager.save(updated)
102+
}
103+
104+
@Transactional
105+
override fun updateCustomPrompt(guideUserId: String, customPrompt: String) {
106+
val guideUser = findById(guideUserId).orElseThrow {
107+
IllegalArgumentException("GuideUser not found: $guideUserId")
108+
}
109+
val updated = guideUser.copy(core = guideUser.core.copy(customPrompt = customPrompt))
110+
graphObjectManager.save(updated)
111+
}
112+
113+
@Transactional(readOnly = true)
114+
override fun findAll(): List<GuideUser> {
115+
return graphObjectManager.loadAll<GuideUser> { }
116+
}
117+
118+
@Transactional
119+
override fun deleteAll() {
120+
graphObjectManager.deleteAll<GuideUser> { }
121+
}
122+
123+
@Transactional
124+
override fun deleteByUsernameStartingWith(prefix: String) {
125+
graphObjectManager.deleteAll<GuideUser> {
126+
where {
127+
query.webUser.userName startsWith prefix
128+
}
129+
}
130+
}
131+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.embabel.guide.domain
2+
3+
import com.embabel.agent.api.identity.User
4+
import org.drivine.annotation.Direction
5+
import org.drivine.annotation.GraphRelationship
6+
import org.drivine.annotation.GraphView
7+
import org.drivine.annotation.Root
8+
9+
/**
10+
* Unified GraphView for GuideUser with optional identity sources.
11+
* A GuideUser may be linked to a WebUser, DiscordUserInfo, both, or neither
12+
* (e.g., when using spring shell).
13+
*/
14+
@GraphView
15+
data class GuideUser(
16+
@Root
17+
val core: GuideUserData,
18+
19+
@GraphRelationship(type = "IS_WEB_USER", direction = Direction.OUTGOING)
20+
val webUser: WebUserData? = null,
21+
22+
@GraphRelationship(type = "IS_DISCORD_USER", direction = Direction.OUTGOING)
23+
val discordUserInfo: DiscordUserInfoData? = null
24+
) : User, HasGuideUserData {
25+
26+
// Helper properties
27+
val isWebUser: Boolean get() = webUser != null
28+
val isDiscordUser: Boolean get() = discordUserInfo != null
29+
val hasIdentitySource: Boolean get() = isWebUser || isDiscordUser
30+
31+
// HasGuideUserData implementation
32+
override fun guideUserData(): GuideUserData = core
33+
34+
// User interface implementation - delegate to available identity source
35+
override val id: String
36+
get() = webUser?.id ?: discordUserInfo?.id ?: core.id
37+
38+
override val displayName: String
39+
get() = webUser?.displayName ?: discordUserInfo?.displayName ?: "Unknown"
40+
41+
override val username: String
42+
get() = webUser?.userName ?: discordUserInfo?.username ?: "unknown"
43+
44+
override val email: String?
45+
get() = webUser?.userEmail
46+
}

src/main/kotlin/com/embabel/guide/domain/GuideUserData.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
package com.embabel.guide.domain
22

33
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
4+
import org.drivine.annotation.NodeFragment
5+
import org.drivine.annotation.NodeId
46

57
/**
6-
* Simple data representation of GuideUser properties for Drivine composition.
7-
* Maps directly to node properties without OGM relationships.
8+
* Node fragment representing a GuideUser in the graph.
89
*/
10+
@NodeFragment(labels = ["GuideUser"])
911
@JsonIgnoreProperties(ignoreUnknown = true)
1012
data class GuideUserData(
13+
@NodeId
1114
var id: String,
1215
var persona: String? = null,
1316
var customPrompt: String? = null

0 commit comments

Comments
 (0)