Skip to content

Commit 9652338

Browse files
committed
Move storage related code from embabel core for cleaner deps.
1 parent 1dbf520 commit 9652338

File tree

11 files changed

+563
-53
lines changed

11 files changed

+563
-53
lines changed

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
<java.version>21</java.version>
2222
<drivine.version>0.0.22</drivine.version>
2323
<kotlin.version>2.0.20</kotlin.version>
24-
<embabel-agent.version>0.3.3-SNAPSHOT</embabel-agent.version>
24+
<embabel-agent.version>0.3.4-SNAPSHOT</embabel-agent.version>
2525
<embabel-common.version>0.1.9-SNAPSHOT</embabel-common.version>
2626
</properties>
2727

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2024-2026 Embabel Pty Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.embabel.chat
17+
18+
/**
19+
* Type of conversation storage.
20+
*/
21+
enum class ConversationStoreType {
22+
/**
23+
* Conversations are stored in memory only.
24+
* Fast and simple, suitable for testing and ephemeral sessions.
25+
*/
26+
IN_MEMORY,
27+
28+
/**
29+
* Conversations are persisted to a backing store.
30+
* The specific store (e.g., Neo4j) is configured at factory level.
31+
*/
32+
STORED
33+
}
34+
35+
/**
36+
* Factory for creating [Conversation] instances.
37+
*
38+
* Implementations provide different storage strategies (in-memory, persistent, etc.).
39+
* Use [ConversationFactoryProvider] to obtain factories by type.
40+
*/
41+
interface ConversationFactory {
42+
43+
/**
44+
* The storage type this factory provides.
45+
*/
46+
val storeType: ConversationStoreType
47+
48+
/**
49+
* Create a new conversation with the given ID.
50+
*
51+
* @param id unique identifier for the conversation
52+
* @return a new Conversation instance
53+
*/
54+
fun create(id: String): Conversation
55+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright 2024-2026 Embabel Pty Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.embabel.chat
17+
18+
/**
19+
* Provider for [ConversationFactory] instances by type.
20+
*
21+
* Implementations resolve factories based on [ConversationStoreType],
22+
* typically backed by Spring beans registered via autoconfiguration.
23+
*/
24+
interface ConversationFactoryProvider {
25+
26+
/**
27+
* Get a conversation factory for the given store type.
28+
*
29+
* @param type the conversation store type
30+
* @return the factory for that type
31+
* @throws IllegalArgumentException if no factory is registered for the type
32+
*/
33+
fun getFactory(type: ConversationStoreType): ConversationFactory
34+
35+
/**
36+
* Get a conversation factory for the given store type, or null if not available.
37+
*
38+
* @param type the conversation store type
39+
* @return the factory for that type, or null
40+
*/
41+
fun getFactoryOrNull(type: ConversationStoreType): ConversationFactory?
42+
43+
/**
44+
* Get all registered factory types.
45+
*/
46+
fun availableTypes(): Set<ConversationStoreType>
47+
}
48+
49+
/**
50+
* Simple map-based implementation of [ConversationFactoryProvider].
51+
*/
52+
class MapConversationFactoryProvider(
53+
private val factories: Map<ConversationStoreType, ConversationFactory>
54+
) : ConversationFactoryProvider {
55+
56+
constructor(vararg factories: ConversationFactory) : this(
57+
factories.associateBy { it.storeType }
58+
)
59+
60+
constructor(factories: List<ConversationFactory>) : this(
61+
factories.associateBy { it.storeType }
62+
)
63+
64+
override fun getFactory(type: ConversationStoreType): ConversationFactory {
65+
return factories[type]
66+
?: throw IllegalArgumentException(
67+
"No ConversationFactory registered for type $type. Available: ${factories.keys}"
68+
)
69+
}
70+
71+
override fun getFactoryOrNull(type: ConversationStoreType): ConversationFactory? {
72+
return factories[type]
73+
}
74+
75+
override fun availableTypes(): Set<ConversationStoreType> = factories.keys
76+
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
/*
2+
* Copyright 2024-2026 Embabel Pty Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.embabel.chat.event
17+
18+
import com.embabel.chat.Message
19+
import com.embabel.chat.Role
20+
import com.embabel.common.core.types.Timestamped
21+
import java.time.Instant
22+
23+
/**
24+
* Status of a message in its lifecycle.
25+
*/
26+
enum class MessageStatus {
27+
/**
28+
* Message has been added to the conversation.
29+
*
30+
* For in-memory conversations, this is the terminal state.
31+
* For persistent conversations, this may be followed by [PERSISTED] or [PERSISTENCE_FAILED].
32+
*/
33+
ADDED,
34+
35+
/**
36+
* Message has been successfully persisted to storage.
37+
*
38+
* Only fired by persistent conversation implementations.
39+
*/
40+
PERSISTED,
41+
42+
/**
43+
* Message persistence failed.
44+
*
45+
* Only fired by persistent conversation implementations.
46+
* Check [MessageEvent.error] for details.
47+
*/
48+
PERSISTENCE_FAILED
49+
}
50+
51+
/**
52+
* Event published for message lifecycle changes in a conversation.
53+
*
54+
* ## Status Flow
55+
*
56+
* | Conversation Type | Events Fired |
57+
* |-------------------|--------------|
58+
* | In-memory | `ADDED` |
59+
* | Persistent | `ADDED` → `PERSISTED` or `PERSISTENCE_FAILED` |
60+
*
61+
* ## Usage
62+
*
63+
* ```kotlin
64+
* @EventListener
65+
* fun onMessage(event: MessageEvent) {
66+
* when (event.status) {
67+
* MessageStatus.ADDED -> {
68+
* // Message appeared in conversation - show in UI
69+
* }
70+
* MessageStatus.PERSISTED -> {
71+
* // Message saved - can update UI indicator
72+
* }
73+
* MessageStatus.PERSISTENCE_FAILED -> {
74+
* // Handle failure - event.error has details
75+
* }
76+
* }
77+
* }
78+
*
79+
* // Or filter to specific status:
80+
* @EventListener(condition = "#event.status.name() == 'ADDED'")
81+
* fun onMessageAdded(event: MessageEvent) { ... }
82+
* ```
83+
*
84+
* @param conversationId the conversation the message belongs to
85+
* @param status the current status of the message
86+
* @param fromUserId the ID of the user who sent this message (author)
87+
* @param toUserId the ID of the user who should receive this message (for routing, e.g., WebSocket)
88+
* @param message the message (always present for ADDED, present for PERSISTED)
89+
* @param content the message content (useful for PERSISTENCE_FAILED when message ref may be stale)
90+
* @param role the message role
91+
* @param error the exception if persistence failed (present for PERSISTENCE_FAILED)
92+
* @param timestamp when the event occurred
93+
*/
94+
data class MessageEvent(
95+
val conversationId: String,
96+
val status: MessageStatus,
97+
val fromUserId: String? = null,
98+
val toUserId: String? = null,
99+
val message: Message? = null,
100+
val content: String? = null,
101+
val role: Role? = null,
102+
val error: Throwable? = null,
103+
override val timestamp: Instant = Instant.now()
104+
) : Timestamped {
105+
106+
companion object {
107+
/**
108+
* Create an ADDED event - message was added to conversation.
109+
*/
110+
fun added(
111+
conversationId: String,
112+
message: Message,
113+
fromUserId: String? = null,
114+
toUserId: String? = null
115+
) = MessageEvent(
116+
conversationId = conversationId,
117+
status = MessageStatus.ADDED,
118+
fromUserId = fromUserId,
119+
toUserId = toUserId,
120+
message = message,
121+
content = message.content,
122+
role = message.role
123+
)
124+
125+
/**
126+
* Create a PERSISTED event - message was saved to storage.
127+
*/
128+
fun persisted(
129+
conversationId: String,
130+
message: Message,
131+
fromUserId: String? = null,
132+
toUserId: String? = null
133+
) = MessageEvent(
134+
conversationId = conversationId,
135+
status = MessageStatus.PERSISTED,
136+
fromUserId = fromUserId,
137+
toUserId = toUserId,
138+
message = message,
139+
content = message.content,
140+
role = message.role
141+
)
142+
143+
/**
144+
* Create a PERSISTENCE_FAILED event.
145+
*/
146+
fun persistenceFailed(
147+
conversationId: String,
148+
content: String,
149+
role: Role,
150+
error: Throwable,
151+
fromUserId: String? = null,
152+
toUserId: String? = null
153+
) = MessageEvent(
154+
conversationId = conversationId,
155+
status = MessageStatus.PERSISTENCE_FAILED,
156+
fromUserId = fromUserId,
157+
toUserId = toUserId,
158+
content = content,
159+
role = role,
160+
error = error
161+
)
162+
}
163+
}

0 commit comments

Comments
 (0)